1
0
Fork 0
mirror of synced 2024-08-31 09:41:13 +12:00

Merge branch 'develop' of github.com:Budibase/budibase into spreadsheet-integration

This commit is contained in:
Andrew Kingston 2023-03-28 15:47:53 +01:00
commit e0e5ca7a3c
190 changed files with 3628 additions and 3305 deletions

View file

@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug, linear labels: bug
assignees: '' assignees: ''
--- ---

View file

@ -56,11 +56,11 @@ 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@v1 - uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml
name: codecov-umbrella name: codecov-umbrella
verbose: true verbose: true

View file

@ -1,6 +1,10 @@
name: "deploy-preprod" name: "deploy-preprod"
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
version:
description: Budibase release version. For example - 1.0.0
required: false
workflow_call: workflow_call:
jobs: jobs:
@ -8,10 +12,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: Get the latest budibase release version
id: version
run: |
if [ -z "${{ github.event.inputs.version }}" ]; then
release_version=$(cat lerna.json | jq -r '.version')
else
release_version=${{ github.event.inputs.version }}
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1 uses: aws-actions/configure-aws-credentials@v1
with: with:
@ -26,7 +36,6 @@ jobs:
-o values.preprod.yaml \ -o values.preprod.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
wc -l values.preprod.yaml wc -l values.preprod.yaml
- name: Deploy to Preprod Environment - name: Deploy to Preprod Environment
uses: budibase/helm@v1.8.0 uses: budibase/helm@v1.8.0
with: with:
@ -37,7 +46,7 @@ jobs:
helm: helm3 helm: helm3
values: | values: |
globals: globals:
appVersion: ${{ steps.previoustag.outputs.tag }} appVersion: v${{ env.RELEASE_VERSION }}
ingress: ingress:
enabled: true enabled: true
nginx: true nginx: true
@ -52,5 +61,5 @@ jobs:
uses: tsickert/discord-webhook@v4.0.0 uses: tsickert/discord-webhook@v4.0.0
with: with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Preprod Deployment Complete: ${{ steps.previoustag.outputs.tag }} deployed to Budibase Pre-prod." content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
embed-title: ${{ steps.previoustag.outputs.tag }} embed-title: ${{ env.RELEASE_VERSION }}

View file

@ -91,9 +91,11 @@ jobs:
uses: azure/setup-helm@v1 uses: azure/setup-helm@v1
id: helm-install id: helm-install
- name: 'Get Previous tag' - name: Get the latest budibase release version
id: previoustag id: version
uses: "WyriHaximus/github-action-get-previous-tag@v1" run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
# due to helm repo index issue: https://github.com/helm/helm/issues/7363 # due to helm repo index issue: https://github.com/helm/helm/issues/7363
# we need to create new package in a different dir, merge the index and move the package back # we need to create new package in a different dir, merge the index and move the package back
@ -116,8 +118,6 @@ jobs:
git add -A git add -A
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
git push git push
env:
RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
deploy-to-legacy-preprod-env: deploy-to-legacy-preprod-env:
needs: [release-images] needs: [release-images]
@ -130,13 +130,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: 'Get Previous tag'
id: previoustag - name: Get the latest budibase release version
uses: "WyriHaximus/github-action-get-previous-tag@v1" id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
PAYLOAD_VERSION: ${{ steps.previoustag.outputs.tag }} PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: budicloud-preprod-deploy event: budicloud-preprod-deploy

View file

@ -62,16 +62,22 @@ spec:
{{ end }} {{ end }}
- name: ENABLE_ANALYTICS - name: ENABLE_ANALYTICS
value: {{ .Values.globals.enableAnalytics | quote }} value: {{ .Values.globals.enableAnalytics | quote }}
- name: API_ENCRYPTION_KEY
value: {{ .Values.globals.apiEncryptionKey | quote }}
- name: INTERNAL_API_KEY - name: INTERNAL_API_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ template "budibase.fullname" . }} name: {{ template "budibase.fullname" . }}
key: internalApiKey key: internalApiKey
- name: INTERNAL_API_KEY_FALLBACK
value: {{ .Values.globals.internalApiKeyFallback | quote }}
- name: JWT_SECRET - name: JWT_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ template "budibase.fullname" . }} name: {{ template "budibase.fullname" . }}
key: jwtSecret key: jwtSecret
- name: JWT_SECRET_FALLBACK
value: {{ .Values.globals.jwtSecretFallback | quote }}
{{ if .Values.services.objectStore.region }} {{ if .Values.services.objectStore.region }}
- name: AWS_REGION - name: AWS_REGION
value: {{ .Values.services.objectStore.region }} value: {{ .Values.services.objectStore.region }}
@ -125,9 +131,9 @@ spec:
- name: SELF_HOSTED - name: SELF_HOSTED
value: {{ .Values.globals.selfHosted | quote }} value: {{ .Values.globals.selfHosted | quote }}
- name: SENTRY_DSN - name: SENTRY_DSN
value: {{ .Values.globals.sentryDSN }} value: {{ .Values.globals.sentryDSN | quote }}
- name: POSTHOG_TOKEN - name: POSTHOG_TOKEN
value: {{ .Values.globals.posthogToken }} value: {{ .Values.globals.posthogToken | quote }}
- name: WORKER_URL - name: WORKER_URL
value: http://worker-service:{{ .Values.services.worker.port }} value: http://worker-service:{{ .Values.services.worker.port }}
- name: PLATFORM_URL - name: PLATFORM_URL
@ -198,8 +204,6 @@ spec:
- name: GLOBAL_AGENT_NO_PROXY - name: GLOBAL_AGENT_NO_PROXY
value: {{ .Values.globals.globalAgentNoProxy | quote }} value: {{ .Values.globals.globalAgentNoProxy | quote }}
{{ end }} {{ end }}
- name: CDN_URL
value: {{ .Values.globals.cdnUrl }}
{{ if .Values.services.tlsRejectUnauthorized }} {{ if .Values.services.tlsRejectUnauthorized }}
- name: NODE_TLS_REJECT_UNAUTHORIZED - name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ .Values.services.tlsRejectUnauthorized }} value: {{ .Values.services.tlsRejectUnauthorized }}
@ -228,6 +232,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
{{ if .Values.imagePullSecrets }} {{ if .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }} {{- toYaml .Values.imagePullSecrets | nindent 6 }}

View file

@ -50,5 +50,8 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
status: {} status: {}
{{- end }} {{- end }}

View file

@ -72,6 +72,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
{{ if .Values.imagePullSecrets }} {{ if .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }} {{- toYaml .Values.imagePullSecrets | nindent 6 }}

View file

@ -78,6 +78,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
{{ if .Values.imagePullSecrets }} {{ if .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }} {{- toYaml .Values.imagePullSecrets | nindent 6 }}

View file

@ -50,6 +50,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
{{ if .Values.imagePullSecrets }} {{ if .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }} {{- toYaml .Values.imagePullSecrets | nindent 6 }}

View file

@ -62,16 +62,22 @@ spec:
{{ else }} {{ else }}
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }} value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}
{{ end }} {{ end }}
- name: API_ENCRYPTION_KEY
value: {{ .Values.globals.apiEncryptionKey | quote }}
- name: INTERNAL_API_KEY - name: INTERNAL_API_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ template "budibase.fullname" . }} name: {{ template "budibase.fullname" . }}
key: internalApiKey key: internalApiKey
- name: INTERNAL_API_KEY_FALLBACK
value: {{ .Values.globals.internalApiKeyFallback | quote }}
- name: JWT_SECRET - name: JWT_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: {{ template "budibase.fullname" . }} name: {{ template "budibase.fullname" . }}
key: jwtSecret key: jwtSecret
- name: JWT_SECRET_FALLBACK
value: {{ .Values.globals.jwtSecretFallback | quote }}
{{ if .Values.services.objectStore.region }} {{ if .Values.services.objectStore.region }}
- name: AWS_REGION - name: AWS_REGION
value: {{ .Values.services.objectStore.region }} value: {{ .Values.services.objectStore.region }}
@ -188,8 +194,6 @@ spec:
- name: GLOBAL_AGENT_NO_PROXY - name: GLOBAL_AGENT_NO_PROXY
value: {{ .Values.globals.globalAgentNoProxy | quote }} value: {{ .Values.globals.globalAgentNoProxy | quote }}
{{ end }} {{ end }}
- name: CDN_URL
value: {{ .Values.globals.cdnUrl }}
{{ if .Values.services.tlsRejectUnauthorized }} {{ if .Values.services.tlsRejectUnauthorized }}
- name: NODE_TLS_REJECT_UNAUTHORIZED - name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ .Values.services.tlsRejectUnauthorized }} value: {{ .Values.services.tlsRejectUnauthorized }}
@ -218,6 +222,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName | quote }}
{{ end }}
{{ if .Values.imagePullSecrets }} {{ if .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }} {{- toYaml .Values.imagePullSecrets | nindent 6 }}

View file

@ -96,9 +96,13 @@ globals:
createSecrets: true # creates an internal API key, JWT secrets and redis password for you createSecrets: true # creates an internal API key, JWT secrets and redis password for you
# if createSecrets is set to false, you can hard-code your secrets here # if createSecrets is set to false, you can hard-code your secrets here
apiEncryptionKey: ""
internalApiKey: "" internalApiKey: ""
jwtSecret: "" jwtSecret: ""
cdnUrl: "" cdnUrl: ""
# fallback values used during live rotation
internalApiKeyFallback: ""
jwtSecretFallback: ""
smtp: smtp:
enabled: false enabled: false

View file

@ -3,6 +3,7 @@ MAIN_PORT=10000
# This section contains all secrets pertaining to the system # This section contains all secrets pertaining to the system
# These should be updated # These should be updated
API_ENCRYPTION_KEY=testsecret
JWT_SECRET=testsecret JWT_SECRET=testsecret
MINIO_ACCESS_KEY=budibase MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase MINIO_SECRET_KEY=budibase

View file

@ -17,6 +17,7 @@ services:
INTERNAL_API_KEY: ${INTERNAL_API_KEY} INTERNAL_API_KEY: ${INTERNAL_API_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002 PORT: 4002
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
LOG_LEVEL: info LOG_LEVEL: info
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131 SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
@ -40,6 +41,7 @@ services:
SELF_HOSTED: 1 SELF_HOSTED: 1
PORT: 4003 PORT: 4003
CLUSTER_PORT: ${MAIN_PORT} CLUSTER_PORT: ${MAIN_PORT}
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}

View file

@ -3,6 +3,7 @@ MAIN_PORT=10000
# This section contains all secrets pertaining to the system # This section contains all secrets pertaining to the system
# These should be updated # These should be updated
API_ENCRYPTION_KEY=testsecret
JWT_SECRET=testsecret JWT_SECRET=testsecret
MINIO_ACCESS_KEY=budibase MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase MINIO_SECRET_KEY=budibase

View file

@ -1,5 +1,5 @@
{ {
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View file

@ -25,6 +25,7 @@
"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",

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"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.12-alpha.3", "@budibase/types": "2.4.27-alpha.9",
"@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",

View file

@ -3,8 +3,8 @@
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
echo "jest --coverage --runInBand" echo "jest --coverage --runInBand --forceExit"
jest --coverage --runInBand jest --coverage --runInBand --forceExit
else else
# --maxWorkers performs better in development # --maxWorkers performs better in development
echo "jest --coverage" echo "jest --coverage"

View file

@ -1,6 +1,5 @@
const _passport = require("koa-passport") const _passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import { Cookie } from "../constants" import { Cookie } from "../constants"
import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { getSessionsForUser, invalidateSessions } from "../security/sessions"
@ -8,7 +7,6 @@ import {
authenticated, authenticated,
csrf, csrf,
google, google,
jwt as jwtPassport,
local, local,
oidc, oidc,
tenancy, tenancy,
@ -21,14 +19,11 @@ import {
OIDCInnerConfig, OIDCInnerConfig,
PlatformLogoutOpts, PlatformLogoutOpts,
SSOProviderType, SSOProviderType,
User,
} from "@budibase/types" } from "@budibase/types"
import { logAlert } from "../logging"
import * as events from "../events" import * as events from "../events"
import * as configs from "../configs" import * as configs from "../configs"
import { clearCookie, getCookie } from "../utils" import { clearCookie, getCookie } from "../utils"
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
import env from "../environment"
const refresh = require("passport-oauth2-refresh") const refresh = require("passport-oauth2-refresh")
export { export {
@ -51,25 +46,6 @@ export const jwt = require("jsonwebtoken")
// Strategies // Strategies
_passport.use(new LocalStrategy(local.options, local.authenticate)) _passport.use(new LocalStrategy(local.options, local.authenticate))
if (jwtPassport.options.secretOrKey) {
_passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate))
} else if (!env.DISABLE_JWT_WARNING) {
logAlert("No JWT Secret supplied, cannot configure JWT strategy")
}
_passport.serializeUser((user: User, done: any) => done(null, user))
_passport.deserializeUser(async (user: User, done: any) => {
const db = getGlobalDB()
try {
const dbUser = await db.get(user._id)
return done(null, dbUser)
} catch (err) {
console.error(`User not found`, err)
return done(null, false, { message: "User not found" })
}
})
async function refreshOIDCAccessToken( async function refreshOIDCAccessToken(
chosenConfig: OIDCInnerConfig, chosenConfig: OIDCInnerConfig,

View file

@ -199,6 +199,10 @@ export class QueryBuilder<T> {
return this return this
} }
setAllOr() {
this.query.allOr = true
}
handleSpaces(input: string) { handleSpaces(input: string) {
if (this.noEscaping) { if (this.noEscaping) {
return input return input
@ -236,6 +240,36 @@ export class QueryBuilder<T> {
return value return value
} }
isMultiCondition() {
let count = 0
for (let filters of Object.values(this.query)) {
// not contains is one massive filter in allOr mode
if (typeof filters === "object") {
count += Object.keys(filters).length
}
}
return count > 1
}
compressFilters(filters: Record<string, string[]>) {
const compressed: typeof filters = {}
for (let key of Object.keys(filters)) {
const finalKey = removeKeyNumbering(key)
if (compressed[finalKey]) {
compressed[finalKey] = compressed[finalKey].concat(filters[key])
} else {
compressed[finalKey] = filters[key]
}
}
// add prefixes back
const final: typeof filters = {}
let count = 1
for (let [key, value] of Object.entries(compressed)) {
final[`${count++}:${key}`] = value
}
return final
}
buildSearchQuery() { buildSearchQuery() {
const builder = this const builder = this
let allOr = this.query && this.query.allOr let allOr = this.query && this.query.allOr
@ -272,9 +306,9 @@ export class QueryBuilder<T> {
} }
const notContains = (key: string, value: any) => { const notContains = (key: string, value: any) => {
// @ts-ignore const allPrefix = allOr ? "*:* AND " : ""
const allPrefix = allOr === "" ? "*:* AND" : "" const mode = allOr ? "AND" : undefined
return allPrefix + "NOT " + contains(key, value) return allPrefix + "NOT " + contains(key, value, mode)
} }
const containsAny = (key: string, value: any) => { const containsAny = (key: string, value: any) => {
@ -299,21 +333,32 @@ export class QueryBuilder<T> {
return `${key}:(${orStatement})` return `${key}:(${orStatement})`
} }
function build(structure: any, queryFn: any) { function build(
structure: any,
queryFn: (key: string, value: any) => string | null,
opts?: { returnBuilt?: boolean; mode?: string }
) {
let built = ""
for (let [key, value] of Object.entries(structure)) { for (let [key, value] of Object.entries(structure)) {
// check for new format - remove numbering if needed // check for new format - remove numbering if needed
key = removeKeyNumbering(key) key = removeKeyNumbering(key)
key = builder.preprocess(builder.handleSpaces(key), { key = builder.preprocess(builder.handleSpaces(key), {
escape: true, escape: true,
}) })
const expression = queryFn(key, value) let expression = queryFn(key, value)
if (expression == null) { if (expression == null) {
continue continue
} }
if (query.length > 0) { if (built.length > 0 || query.length > 0) {
query += ` ${allOr ? "OR" : "AND"} ` const mode = opts?.mode ? opts.mode : allOr ? "OR" : "AND"
built += ` ${mode} `
} }
query += expression built += expression
}
if (opts?.returnBuilt) {
return built
} else {
query += built
} }
} }
@ -384,14 +429,14 @@ export class QueryBuilder<T> {
build(this.query.contains, contains) build(this.query.contains, contains)
} }
if (this.query.notContains) { if (this.query.notContains) {
build(this.query.notContains, notContains) build(this.compressFilters(this.query.notContains), notContains)
} }
if (this.query.containsAny) { if (this.query.containsAny) {
build(this.query.containsAny, containsAny) build(this.query.containsAny, containsAny)
} }
// make sure table ID is always added as an AND // make sure table ID is always added as an AND
if (tableId) { if (tableId) {
query = `(${query})` query = this.isMultiCondition() ? `(${query})` : query
allOr = false allOr = false
build({ tableId }, equal) build({ tableId }, equal)
} }

View file

@ -6,9 +6,13 @@ import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
const INDEX_NAME = "main" const INDEX_NAME = "main"
const index = `function(doc) { const index = `function(doc) {
let props = ["property", "number"] let props = ["property", "number", "array"]
for (let key of props) { for (let key of props) {
if (doc[key]) { if (Array.isArray(doc[key])) {
for (let val of doc[key]) {
index(key, val)
}
} else if (doc[key]) {
index(key, doc[key]) index(key, doc[key])
} }
} }
@ -21,9 +25,14 @@ describe("lucene", () => {
dbName = `db-${newid()}` dbName = `db-${newid()}`
// create the DB for testing // create the DB for testing
db = getDB(dbName) db = getDB(dbName)
await db.put({ _id: newid(), property: "word" }) await db.put({ _id: newid(), property: "word", array: ["1", "4"] })
await db.put({ _id: newid(), property: "word2" }) await db.put({ _id: newid(), property: "word2", array: ["3", "1"] })
await db.put({ _id: newid(), property: "word3", number: 1 }) await db.put({
_id: newid(),
property: "word3",
number: 1,
array: ["1", "2"],
})
}) })
it("should be able to create a lucene index", async () => { it("should be able to create a lucene index", async () => {
@ -118,6 +127,15 @@ describe("lucene", () => {
const resp = await builder.run() const resp = await builder.run()
expect(resp.rows.length).toBe(2) expect(resp.rows.length).toBe(2)
}) })
it("should be able to perform an or not contains search", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addNotContains("array", ["1"])
builder.addNotContains("array", ["2"])
builder.setAllOr()
const resp = await builder.run()
expect(resp.rows.length).toBe(2)
})
}) })
describe("paginated search", () => { describe("paginated search", () => {

View file

@ -30,6 +30,12 @@ const DefaultBucketName = {
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
function getAPIEncryptionKey() {
return process.env.API_ENCRYPTION_KEY
? process.env.API_ENCRYPTION_KEY
: process.env.JWT_SECRET // fallback to the JWT_SECRET used historically
}
const environment = { const environment = {
isTest, isTest,
isJest, isJest,
@ -39,7 +45,9 @@ const environment = {
}, },
JS_BCRYPT: process.env.JS_BCRYPT, JS_BCRYPT: process.env.JS_BCRYPT,
JWT_SECRET: process.env.JWT_SECRET, JWT_SECRET: process.env.JWT_SECRET,
JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
@ -55,6 +63,7 @@ const environment = {
MINIO_URL: process.env.MINIO_URL, MINIO_URL: process.env.MINIO_URL,
MINIO_ENABLED: process.env.MINIO_ENABLED || 1, MINIO_ENABLED: process.env.MINIO_ENABLED || 1,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
INTERNAL_API_KEY_FALLBACK: process.env.INTERNAL_API_KEY_FALLBACK,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL: ACCOUNT_PORTAL_URL:
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",

View file

@ -1,10 +0,0 @@
export class BudibaseError extends Error {
code: string
type: string
constructor(message: string, code: string, type: string) {
super(message)
this.code = code
this.type = type
}
}

View file

@ -1,37 +1,99 @@
import * as licensing from "./licensing" // BASE
// combine all error codes into single object export abstract class BudibaseError extends Error {
code: string
export const codes = { constructor(message: string, code: ErrorCode) {
...licensing.codes, super(message)
this.code = code
}
protected getPublicError?(): any
} }
// combine all error types // ERROR HANDLING
export const types = [licensing.type]
// combine all error contexts export enum ErrorCode {
const context = { USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded",
...licensing.context, FEATURE_DISABLED = "feature_disabled",
INVALID_API_KEY = "invalid_api_key",
HTTP = "http",
} }
// derive a public error message using codes, types and any custom contexts /**
* For the given error, build the public representation that is safe
* to be exposed over an api.
*/
export const getPublicError = (err: any) => { export const getPublicError = (err: any) => {
let error let error
if (err.code || err.type) { if (err.code) {
// add generic error information // add generic error information
error = { error = {
code: err.code, code: err.code,
type: err.type,
} }
if (err.code && context[err.code]) { if (err.getPublicError) {
error = { error = {
...error, ...error,
// get any additional context from this error // get any additional context from this error
...context[err.code](err), ...err.getPublicError(),
} }
} }
} }
return error return error
} }
// HTTP
export class HTTPError extends BudibaseError {
status: number
constructor(message: string, httpStatus: number, code = ErrorCode.HTTP) {
super(message, code)
this.status = httpStatus
}
}
// LICENSING
export class UsageLimitError extends HTTPError {
limitName: string
constructor(message: string, limitName: string) {
super(message, 400, ErrorCode.USAGE_LIMIT_EXCEEDED)
this.limitName = limitName
}
getPublicError() {
return {
limitName: this.limitName,
}
}
}
export class FeatureDisabledError extends HTTPError {
featureName: string
constructor(message: string, featureName: string) {
super(message, 400, ErrorCode.FEATURE_DISABLED)
this.featureName = featureName
}
getPublicError() {
return {
featureName: this.featureName,
}
}
}
// AUTH
export class InvalidAPIKeyError extends BudibaseError {
constructor() {
super(
"Invalid API key - may need re-generated, or user doesn't exist",
ErrorCode.INVALID_API_KEY
)
}
}

View file

@ -1,7 +0,0 @@
import { BudibaseError } from "./base"
export class GenericError extends BudibaseError {
constructor(message: string, code: string, type: string) {
super(message, code, type ? type : "generic")
}
}

View file

@ -1,15 +0,0 @@
import { GenericError } from "./generic"
export class HTTPError extends GenericError {
status: number
constructor(
message: string,
httpStatus: number,
code = "http",
type = "generic"
) {
super(message, code, type)
this.status = httpStatus
}
}

View file

@ -1,3 +1 @@
export * from "./errors" export * from "./errors"
export { UsageLimitError, FeatureDisabledError } from "./licensing"
export { HTTPError } from "./http"

View file

@ -1,39 +0,0 @@
import { HTTPError } from "./http"
export const type = "license_error"
export const codes = {
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
FEATURE_DISABLED: "feature_disabled",
}
export const context = {
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
return {
limitName: err.limitName,
}
},
[codes.FEATURE_DISABLED]: (err: any) => {
return {
featureName: err.featureName,
}
},
}
export class UsageLimitError extends HTTPError {
limitName: string
constructor(message: string, limitName: string) {
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
this.limitName = limitName
}
}
export class FeatureDisabledError extends HTTPError {
featureName: string
constructor(message: string, featureName: string) {
super(message, 400, codes.FEATURE_DISABLED, type)
this.featureName = featureName
}
}

View file

@ -24,6 +24,7 @@ export * as redis from "./redis"
export * as locks from "./redis/redlockImpl" export * as locks from "./redis/redlockImpl"
export * as utils from "./utils" export * as utils from "./utils"
export * as errors from "./errors" export * as errors from "./errors"
export * as timers from "./timers"
export { default as env } from "./environment" export { default as env } from "./environment"
export { SearchParams } from "./db" export { SearchParams } from "./db"
// Add context to tenancy for backwards compatibility // Add context to tenancy for backwards compatibility

View file

@ -1,5 +1,10 @@
import { Cookie, Header } from "../constants" import { Cookie, Header } from "../constants"
import { getCookie, clearCookie, openJwt } from "../utils" import {
getCookie,
clearCookie,
openJwt,
isValidInternalAPIKey,
} from "../utils"
import { getUser } from "../cache/user" import { getUser } from "../cache/user"
import { getSession, updateSessionTTL } from "../security/sessions" import { getSession, updateSessionTTL } from "../security/sessions"
import { buildMatcherRegex, matches } from "./matchers" import { buildMatcherRegex, matches } from "./matchers"
@ -9,6 +14,7 @@ import { decrypt } from "../security/encryption"
import * as identity from "../context/identity" import * as identity from "../context/identity"
import env from "../environment" import env from "../environment"
import { Ctx, EndpointMatcher } from "@budibase/types" import { Ctx, EndpointMatcher } from "@budibase/types"
import { InvalidAPIKeyError, ErrorCode } from "../errors"
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
? parseInt(env.SESSION_UPDATE_PERIOD) ? parseInt(env.SESSION_UPDATE_PERIOD)
@ -35,28 +41,35 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
} }
async function checkApiKey(apiKey: string, populateUser?: Function) { async function checkApiKey(apiKey: string, populateUser?: Function) {
if (apiKey === env.INTERNAL_API_KEY) { // check both the primary and the fallback internal api keys
// this allows for rotation
if (isValidInternalAPIKey(apiKey)) {
return { valid: true } return { valid: true }
} }
const decrypted = decrypt(apiKey) const decrypted = decrypt(apiKey)
const tenantId = decrypted.split(SEPARATOR)[0] const tenantId = decrypted.split(SEPARATOR)[0]
return doInTenant(tenantId, async () => { return doInTenant(tenantId, async () => {
const db = getGlobalDB() let userId
// api key is encrypted in the database try {
const userId = (await queryGlobalView( const db = getGlobalDB()
ViewName.BY_API_KEY, // api key is encrypted in the database
{ userId = (await queryGlobalView(
key: apiKey, ViewName.BY_API_KEY,
}, {
db key: apiKey,
)) as string },
db
)) as string
} catch (err) {
userId = undefined
}
if (userId) { if (userId) {
return { return {
valid: true, valid: true,
user: await getUser(userId, tenantId, populateUser), user: await getUser(userId, tenantId, populateUser),
} }
} else { } else {
throw "Invalid API key" throw new InvalidAPIKeyError()
} }
}) })
} }
@ -157,8 +170,10 @@ export default function (
console.error(`Auth Error: ${err.message}`) console.error(`Auth Error: ${err.message}`)
console.error(err) console.error(err)
// invalid token, clear the cookie // invalid token, clear the cookie
if (err && err.name === "JsonWebTokenError") { if (err?.name === "JsonWebTokenError") {
clearCookie(ctx, Cookie.Auth) clearCookie(ctx, Cookie.Auth)
} else if (err?.code === ErrorCode.INVALID_API_KEY) {
ctx.throw(403, err.message)
} }
// allow configuring for public access // allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) { if ((opts && opts.publicAllowed) || publicEndpoint) {

View file

@ -1,4 +1,3 @@
export * as jwt from "./passport/jwt"
export * as local from "./passport/local" export * as local from "./passport/local"
export * as google from "./passport/sso/google" export * as google from "./passport/sso/google"
export * as oidc from "./passport/sso/oidc" export * as oidc from "./passport/sso/oidc"

View file

@ -1,13 +1,21 @@
import env from "../environment"
import { Header } from "../constants" import { Header } from "../constants"
import { BBContext } from "@budibase/types" import { BBContext } from "@budibase/types"
import { isValidInternalAPIKey } from "../utils"
/** /**
* API Key only endpoint. * API Key only endpoint.
*/ */
export default async (ctx: BBContext, next: any) => { export default async (ctx: BBContext, next: any) => {
const apiKey = ctx.request.headers[Header.API_KEY] const apiKey = ctx.request.headers[Header.API_KEY]
if (apiKey !== env.INTERNAL_API_KEY) { if (!apiKey) {
ctx.throw(403, "Unauthorized")
}
if (Array.isArray(apiKey)) {
ctx.throw(403, "Unauthorized")
}
if (!isValidInternalAPIKey(apiKey)) {
ctx.throw(403, "Unauthorized") ctx.throw(403, "Unauthorized")
} }

View file

@ -1,19 +0,0 @@
import { Cookie } from "../../constants"
import env from "../../environment"
import { authError } from "./utils"
import { BBContext } from "@budibase/types"
export const options = {
secretOrKey: env.JWT_SECRET,
jwtFromRequest: function (ctx: BBContext) {
return ctx.cookies.get(Cookie.Auth)
},
}
export async function authenticate(jwt: Function, done: Function) {
try {
return done(null, jwt)
} catch (err) {
return authError(done, "JWT invalid", err)
}
}

View file

@ -4,6 +4,7 @@ import { JobQueue } from "./constants"
import InMemoryQueue from "./inMemoryQueue" import InMemoryQueue from "./inMemoryQueue"
import BullQueue from "bull" import BullQueue from "bull"
import { addListeners, StalledFn } from "./listeners" import { addListeners, StalledFn } from "./listeners"
import * as timers from "../timers"
const CLEANUP_PERIOD_MS = 60 * 1000 const CLEANUP_PERIOD_MS = 60 * 1000
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
@ -29,8 +30,8 @@ export function createQueue<T>(
} }
addListeners(queue, jobQueue, opts?.removeStalledCb) addListeners(queue, jobQueue, opts?.removeStalledCb)
QUEUES.push(queue) QUEUES.push(queue)
if (!cleanupInterval) { if (!cleanupInterval && !env.isTest()) {
cleanupInterval = setInterval(cleanup, CLEANUP_PERIOD_MS) cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS)
// fire off an initial cleanup // fire off an initial cleanup
cleanup().catch(err => { cleanup().catch(err => {
console.error(`Unable to cleanup automation queue initially - ${err}`) console.error(`Unable to cleanup automation queue initially - ${err}`)
@ -41,7 +42,7 @@ export function createQueue<T>(
export async function shutdown() { export async function shutdown() {
if (cleanupInterval) { if (cleanupInterval) {
clearInterval(cleanupInterval) timers.clear(cleanupInterval)
} }
if (QUEUES.length) { if (QUEUES.length) {
for (let queue of QUEUES) { for (let queue of QUEUES) {

View file

@ -8,6 +8,7 @@ import {
SEPARATOR, SEPARATOR,
SelectableDatabase, SelectableDatabase,
} from "./utils" } from "./utils"
import * as timers from "../timers"
const RETRY_PERIOD_MS = 2000 const RETRY_PERIOD_MS = 2000
const STARTUP_TIMEOUT_MS = 5000 const STARTUP_TIMEOUT_MS = 5000
@ -117,9 +118,9 @@ function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) {
return return
} }
// check if the connection is ready // check if the connection is ready
const interval = setInterval(() => { const interval = timers.set(() => {
if (CONNECTED) { if (CONNECTED) {
clearInterval(interval) timers.clear(interval)
resolve("") resolve("")
} }
}, 500) }, 500)

View file

@ -8,7 +8,7 @@ const RANDOM_BYTES = 16
const STRETCH_LENGTH = 32 const STRETCH_LENGTH = 32
export enum SecretOption { export enum SecretOption {
JWT = "jwt", API = "api",
ENCRYPTION = "encryption", ENCRYPTION = "encryption",
} }
@ -19,10 +19,10 @@ function getSecret(secretOption: SecretOption): string {
secret = env.ENCRYPTION_KEY secret = env.ENCRYPTION_KEY
secretName = "ENCRYPTION_KEY" secretName = "ENCRYPTION_KEY"
break break
case SecretOption.JWT: case SecretOption.API:
default: default:
secret = env.JWT_SECRET secret = env.API_ENCRYPTION_KEY
secretName = "JWT_SECRET" secretName = "API_ENCRYPTION_KEY"
break break
} }
if (!secret) { if (!secret) {
@ -37,7 +37,7 @@ function stretchString(string: string, salt: Buffer) {
export function encrypt( export function encrypt(
input: string, input: string,
secretOption: SecretOption = SecretOption.JWT secretOption: SecretOption = SecretOption.API
) { ) {
const salt = crypto.randomBytes(RANDOM_BYTES) const salt = crypto.randomBytes(RANDOM_BYTES)
const stretched = stretchString(getSecret(secretOption), salt) const stretched = stretchString(getSecret(secretOption), salt)
@ -50,7 +50,7 @@ export function encrypt(
export function decrypt( export function decrypt(
input: string, input: string,
secretOption: SecretOption = SecretOption.JWT secretOption: SecretOption = SecretOption.API
) { ) {
const [salt, encrypted] = input.split(SEPARATOR) const [salt, encrypted] = input.split(SEPARATOR)
const saltBuffer = Buffer.from(salt, "hex") const saltBuffer = Buffer.from(salt, "hex")

View file

@ -0,0 +1 @@
export * from "./timers"

View file

@ -0,0 +1,22 @@
let intervals: NodeJS.Timeout[] = []
export function set(callback: () => any, period: number) {
const interval = setInterval(callback, period)
intervals.push(interval)
return interval
}
export function clear(interval: NodeJS.Timeout) {
const idx = intervals.indexOf(interval)
if (idx !== -1) {
intervals.splice(idx, 1)
}
clearInterval(interval)
}
export function cleanup() {
for (let interval of intervals) {
clearInterval(interval)
}
intervals = []
}

View file

@ -1,5 +1,4 @@
import { getAllApps, queryGlobalView } from "../db" import { getAllApps, queryGlobalView } from "../db"
import { options } from "../middleware/passport/jwt"
import { import {
Header, Header,
MAX_VALID_DATE, MAX_VALID_DATE,
@ -133,7 +132,30 @@ export function openJwt(token: string) {
if (!token) { if (!token) {
return token return token
} }
return jwt.verify(token, options.secretOrKey) try {
return jwt.verify(token, env.JWT_SECRET)
} catch (e) {
if (env.JWT_SECRET_FALLBACK) {
// fallback to enable rotation
return jwt.verify(token, env.JWT_SECRET_FALLBACK)
} else {
throw e
}
}
}
export function isValidInternalAPIKey(apiKey: string) {
if (env.INTERNAL_API_KEY && env.INTERNAL_API_KEY === apiKey) {
return true
}
// fallback to enable rotation
if (
env.INTERNAL_API_KEY_FALLBACK &&
env.INTERNAL_API_KEY_FALLBACK === apiKey
) {
return true
}
return false
} }
/** /**
@ -165,7 +187,7 @@ export function setCookie(
opts = { sign: true } opts = { sign: true }
) { ) {
if (value && opts && opts.sign) { if (value && opts && opts.sign) {
value = jwt.sign(value, options.secretOrKey) value = jwt.sign(value, env.JWT_SECRET)
} }
const config: SetOption = { const config: SetOption = {

View file

@ -4,3 +4,4 @@ process.env.NODE_ENV = "jest"
process.env.MOCK_REDIS = "1" process.env.MOCK_REDIS = "1"
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.ENABLE_4XX_HTTP_LOGGING = "0"
process.env.REDIS_PASSWORD = "budibase"

View file

@ -1,5 +1,6 @@
import "./logging" import "./logging"
import env from "../src/environment" import env from "../src/environment"
import { cleanup } from "../src/timers"
import { mocks, testContainerUtils } from "./utilities" import { mocks, testContainerUtils } from "./utilities"
// must explicitly enable fetch mock // must explicitly enable fetch mock
@ -21,3 +22,7 @@ if (!process.env.CI) {
} }
testContainerUtils.setupEnv(env) testContainerUtils.setupEnv(env)
afterAll(() => {
cleanup()
})

View file

@ -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.12-alpha.3", "version": "2.4.27-alpha.9",
"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.12-alpha.3", "@budibase/shared-core": "2.4.27-alpha.9",
"@budibase/string-templates": "2.4.12-alpha.3", "@budibase/string-templates": "2.4.27-alpha.9",
"@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",

View file

@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -1,56 +0,0 @@
<div class="skeleton">
<div class="children">
<slot />
</div>
</div>
<style>
.skeleton {
height: 100%;
width: 100%;
opacity: 0;
background-color: var(--spectrum-global-color-gray-200) !important;
border-radius: 7px;
overflow: hidden;
position: relative;
animation: fadeIn 130ms ease 0s 1 normal forwards;
}
.children {
pointer-events: none;
opacity: 0;
}
.skeleton::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.15) 20%,
rgba(255, 255, 255, 0.3) 60%,
rgba(255, 255, 255, 0)
);
animation: shimmer 2s infinite;
content: "";
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 0.75;
}
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
</style>

View file

@ -4,7 +4,6 @@ import "./bbui.css"
import "@spectrum-css/icon/dist/index-vars.css" import "@spectrum-css/icon/dist/index-vars.css"
// Components // Components
export { default as Skeleton } from "./Skeleton/Skeleton.svelte"
export { default as Input } from "./Form/Input.svelte" export { default as Input } from "./Form/Input.svelte"
export { default as Stepper } from "./Form/Stepper.svelte" export { default as Stepper } from "./Form/Stepper.svelte"
export { default as TextArea } from "./Form/TextArea.svelte" export { default as TextArea } from "./Form/TextArea.svelte"
@ -78,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"

View file

@ -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>

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -58,11 +58,11 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.4.12-alpha.3", "@budibase/bbui": "2.4.27-alpha.9",
"@budibase/client": "2.4.12-alpha.3", "@budibase/client": "2.4.27-alpha.9",
"@budibase/frontend-core": "2.4.12-alpha.3", "@budibase/frontend-core": "2.4.27-alpha.9",
"@budibase/shared-core": "2.4.12-alpha.3", "@budibase/shared-core": "2.4.27-alpha.9",
"@budibase/string-templates": "2.4.12-alpha.3", "@budibase/string-templates": "2.4.27-alpha.9",
"@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",

View file

@ -308,7 +308,7 @@
{ name: "Auto Column", type: AUTO_TYPE }, { name: "Auto Column", type: AUTO_TYPE },
] ]
} else { } else {
return [ let fields = [
FIELDS.STRING, FIELDS.STRING,
FIELDS.BARCODEQR, FIELDS.BARCODEQR,
FIELDS.LONGFORM, FIELDS.LONGFORM,
@ -316,10 +316,13 @@
FIELDS.DATETIME, FIELDS.DATETIME,
FIELDS.NUMBER, FIELDS.NUMBER,
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.ARRAY,
FIELDS.FORMULA, FIELDS.FORMULA,
FIELDS.LINK,
] ]
// no-sql or a spreadsheet
if (!external || table.sql) {
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
}
return fields
} }
} }

View file

@ -35,7 +35,9 @@
await datasources.fetch() await datasources.fetch()
$goto(`../../table/${table._id}`) $goto(`../../table/${table._id}`)
} catch (error) { } catch (error) {
notifications.error("Error saving table") notifications.error(
`Error saving table - ${error?.message || "unknown error"}`
)
} }
} }
</script> </script>

View file

@ -74,6 +74,14 @@
} }
return capitalise(name) return capitalise(name)
} }
function getDisplayError(error, configKey) {
return error?.replace(
new RegExp(`${configKey}`, "i"),
getDisplayName(configKey)
)
}
function getFieldGroupKeys(fieldGroup) { function getFieldGroupKeys(fieldGroup) {
return Object.entries(schema[fieldGroup].fields || {}) return Object.entries(schema[fieldGroup].fields || {})
.filter(el => filter(el)) .filter(el => filter(el))
@ -147,7 +155,7 @@
type={schema[configKey].type} type={schema[configKey].type}
on:change on:change
bind:value={config[configKey]} bind:value={config[configKey]}
error={$validation.errors[configKey]} error={getDisplayError($validation.errors[configKey], configKey)}
/> />
</div> </div>
{:else if schema[configKey].type === "fieldGroup"} {:else if schema[configKey].type === "fieldGroup"}
@ -180,7 +188,7 @@
type={configKey === "port" ? "string" : schema[configKey].type} type={configKey === "port" ? "string" : schema[configKey].type}
on:change on:change
bind:value={config[configKey]} bind:value={config[configKey]}
error={$validation.errors[configKey]} error={getDisplayError($validation.errors[configKey], configKey)}
environmentVariablesEnabled={$licensing.environmentVariablesEnabled} environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
{handleUpgradePanel} {handleUpgradePanel}
/> />

View file

@ -1,15 +1,22 @@
<script> <script>
import { ModalContent, Body, Layout } from "@budibase/bbui" import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
import { IntegrationNames } from "constants/backend" import { IntegrationNames } from "constants/backend"
import cloneDeep from "lodash/cloneDeepWith" import cloneDeep from "lodash/cloneDeepWith"
import GoogleButton from "../_components/GoogleButton.svelte" import GoogleButton from "../_components/GoogleButton.svelte"
import { saveDatasource as save } from "builderStore/datasource" import { saveDatasource as save } from "builderStore/datasource"
import { organisation } from "stores/portal"
import { onMount } from "svelte"
export let integration export let integration
export let modal export let modal
// kill the reference so the input isn't saved // kill the reference so the input isn't saved
let datasource = cloneDeep(integration) let datasource = cloneDeep(integration)
$: isGoogleConfigured = !!$organisation.google
onMount(async () => {
await organisation.init()
})
</script> </script>
<ModalContent <ModalContent
@ -18,12 +25,21 @@
cancelText="Back" cancelText="Back"
size="L" size="L"
> >
<Layout noPadding> <!-- check true and false directly, don't render until flag is set -->
<Body size="XS" {#if isGoogleConfigured === true}
>Authenticate with your google account to use the {IntegrationNames[ <Layout noPadding>
datasource.type <Body size="S"
]} integration.</Body >Authenticate with your google account to use the {IntegrationNames[
datasource.type
]} integration.</Body
>
</Layout>
<GoogleButton preAuthStep={() => save(datasource, true)} />
{:else if isGoogleConfigured === false}
<Body size="S"
>Google authentication is not enabled, please complete Google SSO
configuration.</Body
> >
</Layout> <Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
<GoogleButton preAuthStep={() => save(datasource, true)} /> {/if}
</ModalContent> </ModalContent>

View file

@ -15,20 +15,12 @@
$: tourKey = $store.tourKey $: tourKey = $store.tourKey
$: tourStepKey = $store.tourStepKey $: tourStepKey = $store.tourStepKey
const initTour = targetKey => { const updateTourStep = (targetStepKey, tourKey) => {
if (!targetKey) { if (!tourKey) {
return return
} }
tourSteps = [...TOURS[targetKey]]
tourStepIdx = 0
tourStep = { ...tourSteps[tourStepIdx] }
}
$: initTour(tourKey)
const updateTourStep = targetStepKey => {
if (!tourSteps?.length) { if (!tourSteps?.length) {
return tourSteps = [...TOURS[tourKey]]
} }
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey) tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
lastStep = tourStepIdx + 1 == tourSteps.length lastStep = tourStepIdx + 1 == tourSteps.length
@ -36,7 +28,7 @@
tourStep.onLoad() tourStep.onLoad()
} }
$: updateTourStep(tourStepKey) $: updateTourStep(tourStepKey, tourKey)
const showPopover = (tourStep, tourNodes, popover) => { const showPopover = (tourStep, tourNodes, popover) => {
if (!tourStep) { if (!tourStep) {

View file

@ -8,20 +8,28 @@
let currentTourStep let currentTourStep
let ready = false let ready = false
let registered = false
let handler let handler
const registerTourNode = (tourKey, stepKey) => {
if (ready && !registered && tourKey) {
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
if (!currentTourStep) {
return
}
const elem = document.querySelector(currentTourStep.query)
handler = tourHandler(elem, stepKey)
registered = true
}
}
$: tourKeyWatch = $store.tourKey
$: registerTourNode(tourKeyWatch, tourStepKey, ready)
onMount(() => { onMount(() => {
if (!$store.tourKey) return
currentTourStep = TOURS[$store.tourKey].find(
step => step.id === tourStepKey
)
if (!currentTourStep) return
const elem = document.querySelector(currentTourStep.query)
handler = tourHandler(elem, tourStepKey)
ready = true ready = true
}) })
onDestroy(() => { onDestroy(() => {
if (handler) { if (handler) {
handler.destroy() handler.destroy()

View file

@ -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>

View file

@ -4,6 +4,7 @@
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
@ -146,6 +147,9 @@
} }
</script> </script>
<!--Portal branding overrides -->
<Branding />
{#if loaded} {#if loaded}
<slot /> <slot />
{/if} {/if}

View file

@ -182,12 +182,13 @@
} }
const handleKeyDown = e => { const handleKeyDown = e => {
if (e.key === "Tab") { if (e.key === "Tab" || e.key === "ArrowDown" || e.key === "ArrowUp") {
// Cycle selected components on tab press // Cycle selected components on tab press
if (selectedIndex == null) { if (selectedIndex == null) {
selectedIndex = 0 selectedIndex = 0
} else { } else {
selectedIndex = (selectedIndex + 1) % componentList.length const direction = e.key === "ArrowUp" ? -1 : 1
selectedIndex = (selectedIndex + direction) % componentList.length
} }
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()

View file

@ -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 {

View file

@ -47,8 +47,9 @@
$: googleCallbackTooltip = $admin.cloud $: googleCallbackTooltip = $admin.cloud
? null ? null
: googleCallbackReadonly : googleCallbackReadonly
? "Vist the organisation page to update the platform URL" ? "Visit the organisation page to update the platform URL"
: "Leave blank to use the default callback URL" : "Leave blank to use the default callback URL"
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
$: GoogleConfigFields = { $: GoogleConfigFields = {
Google: [ Google: [
@ -62,6 +63,14 @@
placeholder: $organisation.googleCallbackUrl, placeholder: $organisation.googleCallbackUrl,
copyButton: true, copyButton: true,
}, },
{
name: "sheetsURL",
label: "Sheets URL",
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: googleSheetsCallbackUrl,
copyButton: true,
},
], ],
} }
@ -396,7 +405,11 @@
</Heading> </Heading>
<Body size="S"> <Body size="S">
To allow users to authenticate using their Google accounts, fill out the To allow users to authenticate using their Google accounts, fill out the
fields below. fields below. Read the <Link
size="M"
href={"https://docs.budibase.com/docs/sso-with-google"}
>documentation</Link
> for more information.
</Body> </Body>
</Layout> </Layout>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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",

View file

@ -6,6 +6,15 @@ 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,

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"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.12-alpha.3", "@budibase/backend-core": "2.4.27-alpha.9",
"@budibase/string-templates": "2.4.12-alpha.3", "@budibase/string-templates": "2.4.27-alpha.9",
"@budibase/types": "2.4.12-alpha.3", "@budibase/types": "2.4.27-alpha.9",
"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",

View file

@ -13,6 +13,7 @@ export const ENV_PATH = path.resolve("./.env")
function getSecrets(opts = { single: false }) { function getSecrets(opts = { single: false }) {
const secrets = [ const secrets = [
"API_ENCRYPTION_KEY",
"JWT_SECRET", "JWT_SECRET",
"MINIO_ACCESS_KEY", "MINIO_ACCESS_KEY",
"MINIO_SECRET_KEY", "MINIO_SECRET_KEY",

View file

@ -4,6 +4,8 @@ import {
downloadDockerCompose, downloadDockerCompose,
handleError, handleError,
getServices, getServices,
getServiceImage,
setServiceImage,
} from "./utils" } from "./utils"
import { confirmation } from "../questions" import { confirmation } from "../questions"
import compose from "docker-compose" import compose from "docker-compose"
@ -23,7 +25,11 @@ export async function update() {
!isSingle && !isSingle &&
(await confirmation("Do you wish to update you docker-compose.yaml?")) (await confirmation("Do you wish to update you docker-compose.yaml?"))
) { ) {
// get current MinIO image
const image = await getServiceImage("minio")
await downloadDockerCompose() await downloadDockerCompose()
// replace MinIO image
setServiceImage("minio", image)
} }
await handleError(async () => { await handleError(async () => {
const status = await compose.ps() const status = await compose.ps()

View file

@ -9,10 +9,44 @@ const ERROR_FILE = "docker-error.log"
const COMPOSE_URL = const COMPOSE_URL =
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml" "https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml"
export async function downloadDockerCompose() { function composeFilename() {
const fileName = COMPOSE_URL.split("/").slice(-1)[0] return COMPOSE_URL.split("/").slice(-1)[0]
}
export function getServiceImage(service: string) {
const filename = composeFilename()
try { try {
await downloadFile(COMPOSE_URL, `./${fileName}`) const { services } = getServices(filename)
const serviceKey = Object.keys(services).find(name =>
name.includes(service)
)
if (serviceKey) {
return services[serviceKey].image
} else {
return null
}
} catch (err) {
return null
}
}
export function setServiceImage(service: string, image: string) {
const filename = composeFilename()
if (!fs.existsSync(filename)) {
throw new Error(
`File ${filename} not found, cannot update ${service} image.`
)
}
const current = getServiceImage(service)!
let contents = fs.readFileSync(filename, "utf8")
contents = contents.replace(`image: ${current}`, `image: ${image}`)
fs.writeFileSync(filename, contents)
}
export async function downloadDockerCompose() {
const filename = composeFilename()
try {
await downloadFile(COMPOSE_URL, `./${filename}`)
} catch (err) { } catch (err) {
console.error(error(`Failed to retrieve compose file - ${err}`)) console.error(error(`Failed to retrieve compose file - ${err}`))
} }
@ -49,6 +83,9 @@ export async function handleError(func: Function) {
} }
export function getServices(path: string) { export function getServices(path: string) {
if (!fs.existsSync(path)) {
throw new Error(`No yaml found at path: ${path}`)
}
const dockerYaml = fs.readFileSync(path, "utf8") const dockerYaml = fs.readFileSync(path, "utf8")
const parsedYaml = yaml.parse(dockerYaml) const parsedYaml = yaml.parse(dockerYaml)
return { yaml: parsedYaml, services: parsedYaml.services } return { yaml: parsedYaml, services: parsedYaml.services }

View file

@ -239,9 +239,9 @@
"@hapi/hoek" "^9.0.0" "@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0": "@sideway/formula@^3.0.0":
version "3.0.0" version "3.0.1"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
"@sideway/pinpoint@^2.0.0": "@sideway/pinpoint@^2.0.0":
version "2.0.0" version "2.0.0"

View file

@ -17,10 +17,7 @@
"description": "This component is specific only to layouts", "description": "This component is specific only to layouts",
"icon": "Sandbox", "icon": "Sandbox",
"hasChildren": true, "hasChildren": true,
"styles": [ "styles": ["padding", "background"],
"padding",
"background"
],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -36,23 +33,14 @@
"type": "select", "type": "select",
"label": "Navigation", "label": "Navigation",
"key": "navigation", "key": "navigation",
"options": [ "options": ["Top", "Left", "None"],
"Top",
"Left",
"None"
],
"defaultValue": "Top" "defaultValue": "Top"
}, },
{ {
"type": "select", "type": "select",
"label": "Width", "label": "Width",
"key": "width", "key": "width",
"options": [ "options": ["Small", "Medium", "Large", "Max"],
"Small",
"Medium",
"Large",
"Max"
],
"defaultValue": "Large" "defaultValue": "Large"
}, },
{ {
@ -89,13 +77,7 @@
"width": 400, "width": 400,
"height": 200 "height": 200
}, },
"styles": [ "styles": ["padding", "size", "background", "border", "shadow"],
"padding",
"size",
"background",
"border",
"shadow"
],
"settings": [ "settings": [
{ {
"type": "select", "type": "select",
@ -255,9 +237,7 @@
"description": "Add a section to your application", "description": "Add a section to your application",
"icon": "ColumnTwoB", "icon": "ColumnTwoB",
"hasChildren": true, "hasChildren": true,
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"showEmptyState": false, "showEmptyState": false,
"size": { "size": {
"width": 400, "width": 400,
@ -376,9 +356,7 @@
"name": "Divider", "name": "Divider",
"description": "A basic divider", "description": "A basic divider",
"icon": "Separator", "icon": "Separator",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 10 "height": 10
@ -415,9 +393,7 @@
"name": "Repeater", "name": "Repeater",
"description": "A configurable data list that attaches to your backend tables.", "description": "A configurable data list that attaches to your backend tables.",
"icon": "JourneyData", "icon": "JourneyData",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"hasChildren": true, "hasChildren": true,
"size": { "size": {
"width": 400, "width": 400,
@ -574,9 +550,7 @@
"name": "Stacked List", "name": "Stacked List",
"icon": "TaskList", "icon": "TaskList",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -606,9 +580,7 @@
"name": "Vertical Card", "name": "Vertical Card",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
"icon": "ViewColumn", "icon": "ViewColumn",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -652,24 +624,14 @@
"type": "select", "type": "select",
"label": "Image Height", "label": "Image Height",
"key": "imageHeight", "key": "imageHeight",
"options": [ "options": ["auto", "12rem", "16rem", "20rem", "24rem"],
"auto",
"12rem",
"16rem",
"20rem",
"24rem"
],
"defaultValue": "auto" "defaultValue": "auto"
}, },
{ {
"type": "select", "type": "select",
"label": "Card Width", "label": "Card Width",
"key": "cardWidth", "key": "cardWidth",
"options": [ "options": ["16rem", "20rem", "24rem"],
"16rem",
"20rem",
"24rem"
],
"defaultValue": "20rem" "defaultValue": "20rem"
} }
] ]
@ -678,9 +640,7 @@
"name": "Paragraph", "name": "Paragraph",
"description": "A component for displaying paragraph text.", "description": "A component for displaying paragraph text.",
"icon": "TextParagraph", "icon": "TextParagraph",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -803,9 +763,7 @@
"name": "Headline", "name": "Headline",
"icon": "TextBold", "icon": "TextBold",
"description": "A component for displaying heading text", "description": "A component for displaying heading text",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -982,9 +940,7 @@
"name": "Image", "name": "Image",
"description": "A basic component for displaying images", "description": "A basic component for displaying images",
"icon": "Image", "icon": "Image",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 300 "height": 300
@ -1002,9 +958,8 @@
"name": "Background Image", "name": "Background Image",
"description": "A background image", "description": "A background image",
"icon": "Images", "icon": "Images",
"styles": [ "hasChildren": true,
"size" "styles": ["size"],
],
"size": { "size": {
"width": 400, "width": 400,
"height": 300 "height": 300
@ -1162,9 +1117,7 @@
"name": "Nav Bar", "name": "Nav Bar",
"description": "A component for handling the navigation within your app.", "description": "A component for handling the navigation within your app.",
"icon": "BreadcrumbNavigation", "icon": "BreadcrumbNavigation",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"hasChildren": true, "hasChildren": true,
"settings": [ "settings": [
{ {
@ -1365,25 +1318,14 @@
"type": "select", "type": "select",
"label": "Image Width", "label": "Image Width",
"key": "imageWidth", "key": "imageWidth",
"options": [ "options": ["auto", "8rem", "12rem", "16rem"],
"auto",
"8rem",
"12rem",
"16rem"
],
"defaultValue": "8rem" "defaultValue": "8rem"
}, },
{ {
"type": "select", "type": "select",
"label": "Image Height", "label": "Image Height",
"key": "imageHeight", "key": "imageHeight",
"options": [ "options": ["auto", "8rem", "12rem", "16rem", "auto"],
"auto",
"8rem",
"12rem",
"16rem",
"auto"
],
"defaultValue": "auto" "defaultValue": "auto"
} }
] ]
@ -1424,9 +1366,7 @@
"name": "Embed", "name": "Embed",
"icon": "Code", "icon": "Code",
"description": "Embed content from 3rd party sources", "description": "Embed content from 3rd party sources",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 100 "height": 100
@ -1478,11 +1418,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -1640,11 +1576,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -1736,11 +1668,7 @@
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
"key": "curve", "key": "curve",
"options": [ "options": ["Smooth", "Straight", "Stepline"],
"Smooth",
"Straight",
"Stepline"
],
"defaultValue": "Smooth" "defaultValue": "Smooth"
}, },
{ {
@ -1801,11 +1729,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -1897,11 +1821,7 @@
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
"key": "curve", "key": "curve",
"options": [ "options": ["Smooth", "Straight", "Stepline"],
"Smooth",
"Straight",
"Stepline"
],
"defaultValue": "Smooth" "defaultValue": "Smooth"
}, },
{ {
@ -2253,11 +2173,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -2293,19 +2209,14 @@
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
"hasChildren": true, "hasChildren": true,
"illegalChildren": [ "illegalChildren": ["section", "form"],
"section",
"form"
],
"actions": [ "actions": [
"ValidateForm", "ValidateForm",
"ClearForm", "ClearForm",
"ChangeFormStep", "ChangeFormStep",
"UpdateFieldValue" "UpdateFieldValue"
], ],
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 400 "height": 400
@ -2315,10 +2226,7 @@
"type": "select", "type": "select",
"label": "Type", "label": "Type",
"key": "actionType", "key": "actionType",
"options": [ "options": ["Create", "Update"],
"Create",
"Update"
],
"defaultValue": "Create" "defaultValue": "Create"
}, },
{ {
@ -2388,14 +2296,8 @@
"name": "Form Step", "name": "Form Step",
"icon": "AssetsAdded", "icon": "AssetsAdded",
"hasChildren": true, "hasChildren": true,
"illegalChildren": [ "illegalChildren": ["section", "form", "form step"],
"section", "styles": ["size"],
"form",
"form step"
],
"styles": [
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 400 "height": 400
@ -2413,12 +2315,8 @@
"fieldgroup": { "fieldgroup": {
"name": "Field Group", "name": "Field Group",
"icon": "Group", "icon": "Group",
"illegalChildren": [ "illegalChildren": ["section"],
"section" "styles": ["size"],
],
"styles": [
"size"
],
"hasChildren": true, "hasChildren": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2448,12 +2346,9 @@
] ]
}, },
"stringfield": { "stringfield": {
"skeleton": false,
"name": "Text Field", "name": "Text Field",
"icon": "Text", "icon": "Text",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2540,12 +2435,9 @@
] ]
}, },
"numberfield": { "numberfield": {
"skeleton": false,
"name": "Number Field", "name": "Number Field",
"icon": "123", "icon": "123",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2598,12 +2490,9 @@
] ]
}, },
"passwordfield": { "passwordfield": {
"skeleton": false,
"name": "Password Field", "name": "Password Field",
"icon": "LockClosed", "icon": "LockClosed",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2656,12 +2545,9 @@
] ]
}, },
"optionsfield": { "optionsfield": {
"skeleton": false,
"name": "Options Picker", "name": "Options Picker",
"icon": "Menu", "icon": "Menu",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2825,12 +2711,9 @@
] ]
}, },
"multifieldselect": { "multifieldselect": {
"skeleton": false,
"name": "Multi-select Picker", "name": "Multi-select Picker",
"icon": "ViewList", "icon": "ViewList",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -2988,7 +2871,6 @@
] ]
}, },
"booleanfield": { "booleanfield": {
"skeleton": false,
"name": "Checkbox", "name": "Checkbox",
"icon": "SelectBox", "icon": "SelectBox",
"editable": true, "editable": true,
@ -3067,12 +2949,9 @@
] ]
}, },
"longformfield": { "longformfield": {
"skeleton": false,
"name": "Long Form Field", "name": "Long Form Field",
"icon": "TextAlignLeft", "icon": "TextAlignLeft",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3147,12 +3026,9 @@
] ]
}, },
"datetimefield": { "datetimefield": {
"skeleton": false,
"name": "Date Picker", "name": "Date Picker",
"icon": "Date", "icon": "Date",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3229,12 +3105,9 @@
] ]
}, },
"codescanner": { "codescanner": {
"skeleton": false,
"name": "Barcode/QR Scanner", "name": "Barcode/QR Scanner",
"icon": "Camera", "icon": "Camera",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 50 "height": 50
@ -3283,9 +3156,7 @@
"embeddedmap": { "embeddedmap": {
"name": "Embedded Map", "name": "Embedded Map",
"icon": "Location", "icon": "Location",
"styles": [ "styles": ["size"],
"size"
],
"draggable": false, "draggable": false,
"size": { "size": {
"width": 400, "width": 400,
@ -3395,12 +3266,9 @@
] ]
}, },
"attachmentfield": { "attachmentfield": {
"skeleton": false,
"name": "Attachment", "name": "Attachment",
"icon": "Attach", "icon": "Attach",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3460,12 +3328,9 @@
] ]
}, },
"relationshipfield": { "relationshipfield": {
"skeleton": false,
"name": "Relationship Picker", "name": "Relationship Picker",
"icon": "TaskList", "icon": "TaskList",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3524,12 +3389,9 @@
] ]
}, },
"jsonfield": { "jsonfield": {
"skeleton": false,
"name": "JSON Field", "name": "JSON Field",
"icon": "Brackets", "icon": "Brackets",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3579,9 +3441,7 @@
"s3upload": { "s3upload": {
"name": "S3 File Upload", "name": "S3 File Upload",
"icon": "UploadToCloud", "icon": "UploadToCloud",
"styles": [ "styles": ["size"],
"size"
],
"editable": true, "editable": true,
"size": { "size": {
"width": 400, "width": 400,
@ -3642,13 +3502,9 @@
"dataprovider": { "dataprovider": {
"name": "Data Provider", "name": "Data Provider",
"icon": "Data", "icon": "Data",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"hasChildren": true, "hasChildren": true,
"actions": [ "actions": ["RefreshDatasource"],
"RefreshDatasource"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 100 "height": 100
@ -3674,10 +3530,7 @@
"type": "select", "type": "select",
"label": "Sort Order", "label": "Sort Order",
"key": "sortOrder", "key": "sortOrder",
"options": [ "options": ["Ascending", "Descending"],
"Ascending",
"Descending"
],
"defaultValue": "Ascending" "defaultValue": "Ascending"
}, },
{ {
@ -3726,12 +3579,9 @@
} }
}, },
"table": { "table": {
"skeleton": false,
"name": "Table", "name": "Table",
"icon": "Table", "icon": "Table",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"hasChildren": true, "hasChildren": true,
"showEmptyState": false, "showEmptyState": false,
"size": { "size": {
@ -3815,9 +3665,7 @@
"daterangepicker": { "daterangepicker": {
"name": "Date Range", "name": "Date Range",
"icon": "Calendar", "icon": "Calendar",
"styles": [ "styles": ["size"],
"size"
],
"hasChildren": false, "hasChildren": false,
"size": { "size": {
"width": 200, "width": 200,
@ -3856,9 +3704,7 @@
"spectrumcard": { "spectrumcard": {
"name": "Card", "name": "Card",
"icon": "PersonalizationField", "icon": "PersonalizationField",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 300, "width": 300,
"height": 120 "height": 120
@ -4031,10 +3877,7 @@
"type": "select", "type": "select",
"label": "Sort Order", "label": "Sort Order",
"key": "sortOrder", "key": "sortOrder",
"options": [ "options": ["Ascending", "Descending"],
"Ascending",
"Descending"
],
"defaultValue": "Ascending" "defaultValue": "Ascending"
}, },
{ {
@ -4213,11 +4056,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -4271,11 +4110,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -4292,11 +4127,7 @@
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
"key": "curve", "key": "curve",
"options": [ "options": ["Smooth", "Straight", "Stepline"],
"Smooth",
"Straight",
"Stepline"
],
"defaultValue": "Smooth" "defaultValue": "Smooth"
} }
] ]
@ -4328,11 +4159,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -4349,11 +4176,7 @@
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
"key": "curve", "key": "curve",
"options": [ "options": ["Smooth", "Straight", "Stepline"],
"Smooth",
"Straight",
"Stepline"
],
"defaultValue": "Smooth" "defaultValue": "Smooth"
}, },
{ {
@ -4418,11 +4241,7 @@
"type": "select", "type": "select",
"label": "Format", "label": "Format",
"key": "yAxisUnits", "key": "yAxisUnits",
"options": [ "options": ["Default", "Thousands", "Millions"],
"Default",
"Thousands",
"Millions"
],
"defaultValue": "Default" "defaultValue": "Default"
}, },
{ {
@ -4443,9 +4262,7 @@
"block": true, "block": true,
"name": "Table block", "name": "Table block",
"icon": "Table", "icon": "Table",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 600, "width": 600,
"height": 400 "height": 400
@ -4483,10 +4300,7 @@
"type": "select", "type": "select",
"label": "Sort Order", "label": "Sort Order",
"key": "sortOrder", "key": "sortOrder",
"options": [ "options": ["Ascending", "Descending"],
"Ascending",
"Descending"
],
"defaultValue": "Ascending" "defaultValue": "Ascending"
}, },
{ {
@ -4638,9 +4452,7 @@
"block": true, "block": true,
"name": "Cards block", "name": "Cards block",
"icon": "PersonalizationField", "icon": "PersonalizationField",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 600, "width": 600,
"height": 400 "height": 400
@ -4679,10 +4491,7 @@
"type": "select", "type": "select",
"label": "Sort Order", "label": "Sort Order",
"key": "sortOrder", "key": "sortOrder",
"options": [ "options": ["Ascending", "Descending"],
"Ascending",
"Descending"
],
"defaultValue": "Descending" "defaultValue": "Descending"
}, },
{ {
@ -4816,9 +4625,7 @@
"block": true, "block": true,
"name": "Repeater block", "name": "Repeater block",
"icon": "ViewList", "icon": "ViewList",
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"hasChildren": true, "hasChildren": true,
"size": { "size": {
"width": 400, "width": 400,
@ -4846,10 +4653,7 @@
"type": "select", "type": "select",
"label": "Sort Order", "label": "Sort Order",
"key": "sortOrder", "key": "sortOrder",
"options": [ "options": ["Ascending", "Descending"],
"Ascending",
"Descending"
],
"defaultValue": "Descending" "defaultValue": "Descending"
}, },
{ {
@ -5044,9 +4848,7 @@
"markdownviewer": { "markdownviewer": {
"name": "Markdown Viewer", "name": "Markdown Viewer",
"icon": "Preview", "icon": "Preview",
"styles": [ "styles": ["size"],
"size"
],
"size": { "size": {
"width": 400, "width": 400,
"height": 100 "height": 100
@ -5063,9 +4865,7 @@
"formblock": { "formblock": {
"name": "Form Block", "name": "Form Block",
"icon": "Form", "icon": "Form",
"styles": [ "styles": ["size"],
"size"
],
"block": true, "block": true,
"info": "Form blocks are only compatible with internal or SQL tables", "info": "Form blocks are only compatible with internal or SQL tables",
"size": { "size": {
@ -5077,11 +4877,7 @@
"type": "select", "type": "select",
"label": "Type", "label": "Type",
"key": "actionType", "key": "actionType",
"options": [ "options": ["Create", "Update", "View"],
"Create",
"Update",
"View"
],
"defaultValue": "Create" "defaultValue": "Create"
}, },
{ {
@ -5215,10 +5011,7 @@
"name": "Side Panel", "name": "Side Panel",
"icon": "RailRight", "icon": "RailRight",
"hasChildren": true, "hasChildren": true,
"illegalChildren": [ "illegalChildren": ["section", "sidepanel"],
"section",
"sidepanel"
],
"showEmptyState": false, "showEmptyState": false,
"draggable": false, "draggable": false,
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action." "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"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.12-alpha.3", "@budibase/bbui": "2.4.27-alpha.9",
"@budibase/frontend-core": "2.4.12-alpha.3", "@budibase/frontend-core": "2.4.27-alpha.9",
"@budibase/shared-core": "2.4.12-alpha.3", "@budibase/shared-core": "2.4.27-alpha.9",
"@budibase/string-templates": "2.4.12-alpha.3", "@budibase/string-templates": "2.4.27-alpha.9",
"@budibase/types": "2.4.12-alpha.3", "@budibase/types": "2.4.27-alpha.9",
"@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",

View file

@ -24,7 +24,6 @@
// to render this part of the block, taking advantage of binding enrichment // to render this part of the block, taking advantage of binding enrichment
$: id = `${block.id}-${context ?? rand}` $: id = `${block.id}-${context ?? rand}`
$: instance = { $: instance = {
_blockElementHasChildren: $$slots?.default ?? false,
_component: `@budibase/standard-components/${type}`, _component: `@budibase/standard-components/${type}`,
_id: id, _id: id,
_instanceName: name || type[0].toUpperCase() + type.slice(1), _instanceName: name || type[0].toUpperCase() + type.slice(1),

View file

@ -29,7 +29,6 @@
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte" import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte" import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
import Skeleton from "components/app/Skeleton.svelte"
export let instance = {} export let instance = {}
export let isLayout = false export let isLayout = false
@ -39,7 +38,6 @@
// Get parent contexts // Get parent contexts
const context = getContext("context") const context = getContext("context")
const loading = getContext("loading")
const insideScreenslot = !!getContext("screenslot") const insideScreenslot = !!getContext("screenslot")
// Create component context // Create component context
@ -172,15 +170,6 @@
$: pad = pad || (interactive && hasChildren && inDndPath) $: pad = pad || (interactive && hasChildren && inDndPath)
$: $dndIsDragging, (pad = false) $: $dndIsDragging, (pad = false)
// Determine whether we should render a skeleton loader for this component
$: showSkeleton =
$loading &&
definition?.name !== "Screenslot" &&
children.length === 0 &&
!instance._blockElementHasChildren &&
!definition?.block &&
definition?.skeleton !== false
// Update component context // Update component context
$: store.set({ $: store.set({
id, id,
@ -507,12 +496,7 @@
}) })
</script> </script>
{#if showSkeleton} {#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
<Skeleton
height={initialSettings?.height || definition?.size?.height || 0}
width={initialSettings?.width || definition?.size?.width || 0}
/>
{:else if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
<!-- The ID is used as a class because getElementsByClassName is O(1) --> <!-- The ID is used as a class because getElementsByClassName is O(1) -->
<!-- and the performance matters for the selection indicators --> <!-- and the performance matters for the selection indicators -->
<div <div
@ -530,23 +514,25 @@
data-icon={icon} data-icon={icon}
data-parent={parent} data-parent={parent}
> >
<svelte:component this={constructor} bind:this={ref} {...initialSettings}> {#if hasMissingRequiredSettings}
{#if hasMissingRequiredSettings} <ComponentPlaceholder />
<ComponentPlaceholder /> {:else}
{:else if children.length} <svelte:component this={constructor} bind:this={ref} {...initialSettings}>
{#each children as child (child._id)} {#if children.length}
<svelte:self instance={child} parent={id} /> {#each children as child (child._id)}
{/each} <svelte:self instance={child} parent={id} />
{:else if emptyState} {/each}
{#if isScreen} {:else if emptyState}
<ScreenPlaceholder /> {#if isScreen}
{:else} <ScreenPlaceholder />
<Placeholder /> {:else}
<Placeholder />
{/if}
{:else if isBlock}
<slot />
{/if} {/if}
{:else if isBlock} </svelte:component>
<slot /> {/if}
{/if}
</svelte:component>
</div> </div>
{/if} {/if}

View file

@ -1,5 +1,4 @@
<script> <script>
import { writable } from "svelte/store"
import { setContext, getContext, onMount } from "svelte" import { setContext, getContext, onMount } from "svelte"
import Router, { querystring } from "svelte-spa-router" import Router, { querystring } from "svelte-spa-router"
import { routeStore, stateStore } from "stores" import { routeStore, stateStore } from "stores"
@ -10,9 +9,6 @@
const component = getContext("component") const component = getContext("component")
setContext("screenslot", true) setContext("screenslot", true)
const loading = writable(false)
setContext("loading", loading)
// Only wrap this as an array to take advantage of svelte keying, // Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config // to ensure the svelte-spa-router is fully remounted when route config
// changes // changes

View file

@ -21,7 +21,9 @@
{#if url} {#if url}
<div class="outer" use:styleable={$component.styles}> <div class="outer" use:styleable={$component.styles}>
<div class="inner" {style} /> <div class="inner" {style}>
<slot />
</div>
</div> </div>
{:else if $builderStore.inBuilder} {:else if $builderStore.inBuilder}
<div <div

View file

@ -3,12 +3,13 @@
import { builderStore } from "stores" import { builderStore } from "stores"
const component = getContext("component") const component = getContext("component")
const { styleable } = getContext("sdk")
$: requiredSetting = $component.missingRequiredSettings?.[0] $: requiredSetting = $component.missingRequiredSettings?.[0]
</script> </script>
{#if $builderStore.inBuilder && requiredSetting} {#if $builderStore.inBuilder && requiredSetting}
<div class="component-placeholder"> <div class="component-placeholder" use:styleable={$component.styles}>
<span> <span>
Add the <mark>{requiredSetting.label}</mark> setting to start using your component Add the <mark>{requiredSetting.label}</mark> setting to start using your component
- -
@ -32,7 +33,7 @@
} }
.component-placeholder mark { .component-placeholder mark {
background-color: var(--spectrum-global-color-gray-400); background-color: var(--spectrum-global-color-gray-400);
padding: 0 2px; padding: 0 4px;
border-radius: 2px; border-radius: 2px;
} }
.component-placeholder .spectrum-Link { .component-placeholder .spectrum-Link {

View file

@ -1,7 +1,6 @@
<script> <script>
import { writable } from "svelte/store" import { getContext } from "svelte"
import { setContext, getContext } from "svelte" import { Pagination, ProgressCircle } from "@budibase/bbui"
import { Pagination } from "@budibase/bbui"
import { fetchData, LuceneUtils } from "@budibase/frontend-core" import { fetchData, LuceneUtils } from "@budibase/frontend-core"
export let dataSource export let dataSource
@ -14,11 +13,6 @@
const { styleable, Provider, ActionTypes, API } = getContext("sdk") const { styleable, Provider, ActionTypes, API } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// Update loading state
const parentLoading = getContext("loading")
const loading = writable(true)
setContext("loading", loading)
// We need to manage our lucene query manually as we want to allow components // We need to manage our lucene query manually as we want to allow components
// to extend it // to extend it
let queryExtensions = {} let queryExtensions = {}
@ -26,8 +20,8 @@
$: query = extendQuery(defaultQuery, queryExtensions) $: query = extendQuery(defaultQuery, queryExtensions)
// Fetch data and refresh when needed // Fetch data and refresh when needed
$: fetch = createFetch(dataSource, $parentLoading) $: fetch = createFetch(dataSource)
$: updateFetch({ $: fetch.update({
query, query,
sortColumn, sortColumn,
sortOrder, sortOrder,
@ -35,9 +29,6 @@
paginate, paginate,
}) })
// Keep loading context updated
$: loading.set($parentLoading || !$fetch.loaded)
// Build our action context // Build our action context
$: actions = [ $: actions = [
{ {
@ -89,18 +80,7 @@
limit, limit,
} }
const createFetch = (datasource, parentLoading) => { const createFetch = datasource => {
// Return a dummy fetch if parent is still loading. We do this so that we
// can still properly subscribe to a valid fetch object and check all
// properties, but we want to avoid fetching the real data until all parents
// have finished loading.
// This logic is only needed due to skeleton loaders, as previously we
// simply blocked component rendering until data was ready.
if (parentLoading) {
return fetchData({ API })
}
// Otherwise return the real thing
return fetchData({ return fetchData({
API, API,
datasource, datasource,
@ -114,14 +94,6 @@
}) })
} }
const updateFetch = opts => {
// Only update fetch if parents have stopped loading. Otherwise we will
// trigger a fetch of the real data before parents are ready.
if (!$parentLoading) {
fetch.update(opts)
}
}
const addQueryExtension = (key, extension) => { const addQueryExtension = (key, extension) => {
if (!key || !extension) { if (!key || !extension) {
return return
@ -155,17 +127,23 @@
<div use:styleable={$component.styles} class="container"> <div use:styleable={$component.styles} class="container">
<Provider {actions} data={dataContext}> <Provider {actions} data={dataContext}>
<slot /> {#if !$fetch.loaded}
{#if paginate && $fetch.supportsPagination} <div class="loading">
<div class="pagination"> <ProgressCircle />
<Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$fetch.hasNextPage}
goToPrevPage={fetch.prevPage}
goToNextPage={fetch.nextPage}
/>
</div> </div>
{:else}
<slot />
{#if paginate && $fetch.supportsPagination}
<div class="pagination">
<Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$fetch.hasNextPage}
goToPrevPage={fetch.prevPage}
goToNextPage={fetch.nextPage}
/>
</div>
{/if}
{/if} {/if}
</Provider> </Provider>
</div> </div>
@ -177,6 +155,13 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
} }
.loading {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100px;
}
.pagination { .pagination {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -12,25 +12,22 @@
const { Provider } = getContext("sdk") const { Provider } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
const loading = getContext("loading")
// If the parent DataProvider is loading, fill the rows array with a number of empty objects corresponding to the DataProvider's page size; this allows skeleton loader components to be rendered further down the tree. $: rows = dataProvider?.rows ?? []
$: rows = $loading $: loaded = dataProvider?.loaded ?? true
? new Array(dataProvider.limit > 20 ? 20 : dataProvider.limit).fill({})
: dataProvider?.rows
</script> </script>
<Container {direction} {hAlign} {vAlign} {gap} wrap> <Container {direction} {hAlign} {vAlign} {gap} wrap>
{#if $component.empty} {#if $component.empty}
<Placeholder /> <Placeholder />
{:else if !$loading && rows.length === 0} {:else if rows.length > 0}
<div class="noRows"><i class="ri-list-check-2" />{noRowsMessage}</div>
{:else}
{#each rows as row, index} {#each rows as row, index}
<Provider data={{ ...row, index }}> <Provider data={{ ...row, index }}>
<slot /> <slot />
</Provider> </Provider>
{/each} {/each}
{:else if loaded && noRowsMessage}
<div class="noRows"><i class="ri-list-check-2" />{noRowsMessage}</div>
{/if} {/if}
</Container> </Container>

View file

@ -1,31 +0,0 @@
<script>
import { getContext } from "svelte"
import { Skeleton } from "@budibase/bbui"
const { styleable } = getContext("sdk")
const component = getContext("component")
export let height
export let width
let styles
$: {
styles = JSON.parse(JSON.stringify($component.styles))
if (!styles.normal.height && height) {
// The height and width props provided to this component can either be numbers or strings set by users (ex. '100%', '100px', '100'). A string of '100' wouldn't be a valid CSS property, but some of our components respect that input, so we need to handle it here also, hence the `!isNaN` check.
styles.normal.height = !isNaN(height) ? `${height}px` : height
}
if (!styles.normal.width && width) {
styles.normal.width = !isNaN(width) ? `${width}px` : width
}
}
</script>
<div use:styleable={styles}>
<Skeleton>
<slot />
</Skeleton>
</div>

View file

@ -37,6 +37,7 @@
let repeaterId let repeaterId
let schema let schema
let enrichedSearchColumns let enrichedSearchColumns
let schemaLoaded = false
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then( $: enrichSearchColumns(searchColumns, schema).then(
@ -77,135 +78,138 @@
enrichRelationships: true, enrichRelationships: true,
}) })
} }
schemaLoaded = true
} }
</script> </script>
<Block> {#if schemaLoaded}
<BlockComponent <Block>
type="form" <BlockComponent
bind:id={formId} type="form"
props={{ dataSource, disableValidation: true }} bind:id={formId}
> props={{ dataSource, disableValidation: true }}
{#if title || enrichedSearchColumns?.length || showTitleButton} >
<BlockComponent {#if title || enrichedSearchColumns?.length || showTitleButton}
type="container"
props={{
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent <BlockComponent
type="container" type="container"
props={{ props={{
direction: "row", direction: "row",
hAlign: "left", hAlign: "stretch",
vAlign: "middle", vAlign: "middle",
gap: "M", gap: "M",
wrap: true, wrap: true,
}} }}
order={1}
>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
order={idx}
styles={{
normal: {
width: "192px",
},
}}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
}}
order={1}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
}}
order={0}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{ styles={{
normal: { normal: {
width: "auto", "margin-bottom": "20px",
}, },
}} }}
order={0} order={0}
/> >
<BlockComponent
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "left",
vAlign: "middle",
gap: "M",
wrap: true,
}}
order={1}
>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
order={idx}
styles={{
normal: {
width: "192px",
},
}}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
}}
order={1}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
}}
order={0}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
normal: {
width: "auto",
},
}}
order={0}
/>
</BlockComponent>
</BlockComponent> </BlockComponent>
</BlockComponent> </BlockComponent>
</BlockComponent> </Block>
</Block> {/if}

View file

@ -37,6 +37,7 @@
let schema let schema
let primaryDisplay let primaryDisplay
let enrichedSearchColumns let enrichedSearchColumns
let schemaLoaded = false
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then( $: enrichSearchColumns(searchColumns, schema).then(
@ -91,6 +92,7 @@
enrichRelationships: true, enrichRelationships: true,
}) })
} }
schemaLoaded = true
} }
const getNormalFields = schema => { const getNormalFields = schema => {
@ -114,160 +116,162 @@
} }
</script> </script>
<Block> {#if schemaLoaded}
<BlockComponent <Block>
type="form" <BlockComponent
bind:id={formId} type="form"
props={{ bind:id={formId}
dataSource, props={{
disableValidation: true, dataSource,
editAutoColumns: true, disableValidation: true,
size, editAutoColumns: true,
}} size,
> }}
{#if title || enrichedSearchColumns?.length || showTitleButton} >
<BlockComponent {#if title || enrichedSearchColumns?.length || showTitleButton}
type="container"
props={{
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent <BlockComponent
type="container" type="container"
props={{ props={{
direction: "row", direction: "row",
hAlign: "left", hAlign: "stretch",
vAlign: "center", vAlign: "middle",
gap: "M", gap: "M",
wrap: true, wrap: true,
}} }}
order={1} styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
> >
{#if enrichedSearchColumns?.length} <BlockComponent
{#each enrichedSearchColumns as column, idx} type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "left",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={1}
>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
styles={{
normal: {
width: "192px",
},
}}
order={idx}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent <BlockComponent
type={column.componentType} type="button"
props={{ props={{
field: column.name, onClick: buttonClickActions,
placeholder: column.name, text: titleButtonText,
text: column.name, type: "cta",
autoWidth: true,
}} }}
styles={{ order={enrichedSearchColumns?.length ?? 0}
normal: {
width: "192px",
},
}}
order={idx}
/> />
{/each} {/if}
{/if} </BlockComponent>
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: buttonClickActions,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent> </BlockComponent>
</BlockComponent> {/if}
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn: sortColumn || primaryDisplay,
sortOrder,
paginate,
limit: rowCount,
}}
order={1}
>
<BlockComponent <BlockComponent
type="table" type="dataprovider"
context="table" bind:id={dataProviderId}
props={{ props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`, dataSource,
columns: tableColumns, filter: enrichedFilter,
rowCount, sortColumn: sortColumn || primaryDisplay,
quiet, sortOrder,
compact, paginate,
allowSelectRows, limit: rowCount,
size,
onClick: rowClickActions,
}} }}
/> order={1}
>
<BlockComponent
type="table"
context="table"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
rowCount,
quiet,
compact,
allowSelectRows,
size,
onClick: rowClickActions,
}}
/>
</BlockComponent>
{#if clickBehaviour === "details"}
<BlockComponent
name="Details side panel"
type="sidepanel"
bind:id={detailsSidePanelId}
context="details-side-panel"
order={2}
>
<BlockComponent
name="Details form block"
type="formblock"
bind:id={detailsFormBlockId}
props={{
dataSource,
showSaveButton: true,
showDeleteButton: true,
actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: normalFields,
title: editTitle,
labelPosition: "left",
}}
/>
</BlockComponent>
{/if}
{#if showTitleButton && titleButtonClickBehaviour === "new"}
<BlockComponent
name="New row side panel"
type="sidepanel"
bind:id={newRowSidePanelId}
context="new-side-panel"
order={3}
>
<BlockComponent
name="New row form block"
type="formblock"
props={{
dataSource,
showSaveButton: true,
showDeleteButton: false,
actionType: "Create",
fields: normalFields,
title: "Create Row",
labelPosition: "left",
}}
/>
</BlockComponent>
{/if}
</BlockComponent> </BlockComponent>
{#if clickBehaviour === "details"} </Block>
<BlockComponent {/if}
name="Details side panel"
type="sidepanel"
bind:id={detailsSidePanelId}
context="details-side-panel"
order={2}
>
<BlockComponent
name="Details form block"
type="formblock"
bind:id={detailsFormBlockId}
props={{
dataSource,
showSaveButton: true,
showDeleteButton: true,
actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: normalFields,
title: editTitle,
labelPosition: "left",
}}
/>
</BlockComponent>
{/if}
{#if showTitleButton && titleButtonClickBehaviour === "new"}
<BlockComponent
name="New row side panel"
type="sidepanel"
bind:id={newRowSidePanelId}
context="new-side-panel"
order={3}
>
<BlockComponent
name="New row form block"
type="formblock"
props={{
dataSource,
showSaveButton: true,
showDeleteButton: false,
actionType: "Create",
fields: normalFields,
title: "Create Row",
labelPosition: "left",
}}
/>
</BlockComponent>
{/if}
</BlockComponent>
</Block>

View file

@ -1,7 +1,6 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte" import FieldGroupFallback from "./FieldGroupFallback.svelte"
import Skeleton from "../Skeleton.svelte"
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
export let label export let label
@ -54,8 +53,6 @@
builderStore.actions.updateProp("label", e.target.textContent) builderStore.actions.updateProp("label", e.target.textContent)
} }
const loading = getContext("loading")
onDestroy(() => { onDestroy(() => {
fieldApi?.deregister() fieldApi?.deregister()
unsubscribe?.() unsubscribe?.()
@ -79,10 +76,6 @@
<div class="spectrum-Form-itemField"> <div class="spectrum-Form-itemField">
{#if !formContext} {#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" /> <Placeholder text="Form components need to be wrapped in a form" />
{:else if $loading}
<Skeleton>
<slot />
</Skeleton>
{:else if !fieldState} {:else if !fieldState}
<Placeholder /> <Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)} {:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}

View file

@ -1,8 +1,7 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext } from "svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
export let dataSource export let dataSource
export let theme export let theme
@ -21,11 +20,6 @@
const context = getContext("context") const context = getContext("context")
const { API, fetchDatasourceSchema } = getContext("sdk") const { API, fetchDatasourceSchema } = getContext("sdk")
// Forms also use loading context as they require loading a schema
const parentLoading = getContext("loading")
const loading = writable(true)
setContext("loading", loading)
let loaded = false let loaded = false
let schema let schema
let table let table
@ -36,7 +30,6 @@
$: resetKey = Helpers.hashString( $: resetKey = Helpers.hashString(
schemaKey + JSON.stringify(initialValues) + disabled schemaKey + JSON.stringify(initialValues) + disabled
) )
$: loading.set($parentLoading || !loaded)
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, context) => { const getInitialValues = (type, dataSource, context) => {
@ -86,19 +79,21 @@
} }
</script> </script>
{#key resetKey} {#if loaded}
<InnerForm {#key resetKey}
{dataSource} <InnerForm
{theme} {dataSource}
{size} {theme}
{disabled} {size}
{actionType} {disabled}
{schema} {actionType}
{table} {schema}
{initialValues} {table}
{disableValidation} {initialValues}
{editAutoColumns} {disableValidation}
> {editAutoColumns}
<slot /> >
</InnerForm> <slot />
{/key} </InnerForm>
{/key}
{/if}

View file

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Table, Skeleton } from "@budibase/bbui" import { Table } from "@budibase/bbui"
import SlotRenderer from "./SlotRenderer.svelte" import SlotRenderer from "./SlotRenderer.svelte"
import { UnsortableTypes } from "../../../constants" import { UnsortableTypes } from "../../../constants"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
@ -14,7 +14,6 @@
export let compact export let compact
export let onClick export let onClick
const loading = getContext("loading")
const component = getContext("component") const component = getContext("component")
const { styleable, getAction, ActionTypes, rowSelectionStore } = const { styleable, getAction, ActionTypes, rowSelectionStore } =
getContext("sdk") getContext("sdk")
@ -29,6 +28,7 @@
let selectedRows = [] let selectedRows = []
$: hasChildren = $component.children $: hasChildren = $component.children
$: loading = dataProvider?.loading ?? false
$: data = dataProvider?.rows || [] $: data = dataProvider?.rows || []
$: fullSchema = dataProvider?.schema ?? {} $: fullSchema = dataProvider?.schema ?? {}
$: fields = getFields(fullSchema, columns, false) $: fields = getFields(fullSchema, columns, false)
@ -130,7 +130,7 @@
<Table <Table
{data} {data}
{schema} {schema}
loading={$loading} {loading}
{rowCount} {rowCount}
{quiet} {quiet}
{compact} {compact}
@ -145,9 +145,6 @@
on:sort={onSort} on:sort={onSort}
on:click={handleClick} on:click={handleClick}
> >
<div class="skeleton" slot="loadingIndicator">
<Skeleton />
</div>
<slot /> <slot />
</Table> </Table>
{#if allowSelectRows && selectedRows.length} {#if allowSelectRows && selectedRows.length}
@ -161,12 +158,6 @@
div { div {
background-color: var(--spectrum-alias-background-color-secondary); background-color: var(--spectrum-alias-background-color-secondary);
} }
.skeleton {
height: 100%;
width: 100%;
}
.row-count { .row-count {
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
} }

View file

@ -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,

View file

@ -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()
} }

View file

@ -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()

View file

@ -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",

View file

@ -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"

View file

@ -1,13 +1,13 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"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.12-alpha.3", "@budibase/bbui": "2.4.27-alpha.9",
"@budibase/shared-core": "2.4.12-alpha.3", "@budibase/shared-core": "2.4.27-alpha.9",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/sdk", "name": "@budibase/sdk",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"description": "Budibase Public API SDK", "description": "Budibase Public API SDK",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",

View file

@ -4,6 +4,7 @@ module FetchMock {
// @ts-ignore // @ts-ignore
const fetch = jest.requireActual("node-fetch") const fetch = jest.requireActual("node-fetch")
let failCount = 0 let failCount = 0
let mockSearch = false
const func = async (url: any, opts: any) => { const func = async (url: any, opts: any) => {
function json(body: any, status = 200) { function json(body: any, status = 200) {
@ -69,7 +70,7 @@ module FetchMock {
}, },
404 404
) )
} else if (url.includes("_search")) { } else if (mockSearch && url.includes("_search")) {
const body = opts.body const body = opts.body
const parts = body.split("tableId:") const parts = body.split("tableId:")
let tableId let tableId
@ -192,5 +193,9 @@ module FetchMock {
func.Headers = fetch.Headers func.Headers = fetch.Headers
func.mockSearch = () => {
mockSearch = true
}
module.exports = func module.exports = func
} }

View file

@ -43,6 +43,7 @@ const config: Config.InitialOptions = {
"../backend-core/src/**/*.{js,ts}", "../backend-core/src/**/*.{js,ts}",
// 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}",
], ],
coverageReporters: ["lcov", "json", "clover"], coverageReporters: ["lcov", "json", "clover"],
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "2.4.12-alpha.3", "version": "2.4.27-alpha.9",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -14,7 +14,8 @@
"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": "bash scripts/test.sh", "test": "NODE_OPTIONS=\"--max-old-space-size=4096\" bash scripts/test.sh",
"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",
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION", "build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
@ -43,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.12-alpha.3", "@budibase/backend-core": "2.4.27-alpha.9",
"@budibase/client": "2.4.12-alpha.3", "@budibase/client": "2.4.27-alpha.9",
"@budibase/pro": "2.4.12-alpha.3", "@budibase/pro": "2.4.27-alpha.9",
"@budibase/shared-core": "2.4.12-alpha.3", "@budibase/shared-core": "2.4.27-alpha.9",
"@budibase/string-templates": "2.4.12-alpha.3", "@budibase/string-templates": "2.4.27-alpha.9",
"@budibase/types": "2.4.12-alpha.3", "@budibase/types": "2.4.27-alpha.9",
"@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",
@ -126,7 +127,7 @@
"@babel/core": "7.17.4", "@babel/core": "7.17.4",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@jest/test-sequencer": "24.9.0", "@jest/test-sequencer": "29.5.0",
"@swc/core": "^1.3.25", "@swc/core": "^1.3.25",
"@swc/jest": "^0.2.24", "@swc/jest": "^0.2.24",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "^2.1.1",
@ -135,7 +136,7 @@
"@types/global-agent": "2.1.1", "@types/global-agent": "2.1.1",
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "27.5.1", "@types/jest": "29.5.0",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.180",
@ -155,7 +156,7 @@
"eslint": "6.8.0", "eslint": "6.8.0",
"ioredis-mock": "7.2.0", "ioredis-mock": "7.2.0",
"is-wsl": "2.2.0", "is-wsl": "2.2.0",
"jest": "28.1.1", "jest": "29.5.0",
"jest-openapi": "0.14.2", "jest-openapi": "0.14.2",
"jest-serial-runner": "^1.2.1", "jest-serial-runner": "^1.2.1",
"nodemon": "2.0.15", "nodemon": "2.0.15",
@ -167,7 +168,7 @@
"supertest": "6.2.2", "supertest": "6.2.2",
"swagger-jsdoc": "6.1.0", "swagger-jsdoc": "6.1.0",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-jest": "28.0.4", "ts-jest": "29.0.5",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "4.7.3",

View file

@ -3,8 +3,8 @@
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
echo "jest --coverage --runInBand" echo "jest --coverage --runInBand --forceExit"
jest --coverage --runInBand 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"

View file

@ -20,10 +20,10 @@ import {
cache, cache,
tenancy, tenancy,
context, context,
errors,
events, events,
migrations, migrations,
objectStore, objectStore,
ErrorCode,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { USERS_TABLE_SCHEMA } from "../../constants" import { USERS_TABLE_SCHEMA } from "../../constants"
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default" import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
@ -44,7 +44,6 @@ import {
Layout, Layout,
Screen, Screen,
MigrationType, MigrationType,
BBContext,
Database, Database,
UserCtx, UserCtx,
} from "@budibase/types" } from "@budibase/types"
@ -74,14 +73,14 @@ async function getScreens() {
).rows.map((row: any) => row.doc) ).rows.map((row: any) => row.doc)
} }
function getUserRoleId(ctx: BBContext) { function getUserRoleId(ctx: UserCtx) {
return !ctx.user?.role || !ctx.user.role._id return !ctx.user?.role || !ctx.user.role._id
? roles.BUILTIN_ROLE_IDS.PUBLIC ? roles.BUILTIN_ROLE_IDS.PUBLIC
: ctx.user.role._id : ctx.user.role._id
} }
function checkAppUrl( function checkAppUrl(
ctx: BBContext, ctx: UserCtx,
apps: App[], apps: App[],
url: string, url: string,
currentAppId?: string currentAppId?: string
@ -95,7 +94,7 @@ function checkAppUrl(
} }
function checkAppName( function checkAppName(
ctx: BBContext, ctx: UserCtx,
apps: App[], apps: App[],
name: string, name: string,
currentAppId?: string currentAppId?: string
@ -160,7 +159,7 @@ async function addDefaultTables(db: Database) {
await db.bulkDocs([...defaultDbDocs]) await db.bulkDocs([...defaultDbDocs])
} }
export async function fetch(ctx: BBContext) { export async function fetch(ctx: UserCtx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = (await dbCore.getAllApps({ dev, all })) as App[] const apps = (await dbCore.getAllApps({ dev, all })) as App[]
@ -185,7 +184,7 @@ export async function fetch(ctx: BBContext) {
ctx.body = await checkAppMetadata(apps) ctx.body = await checkAppMetadata(apps)
} }
export async function fetchAppDefinition(ctx: BBContext) { export async function fetchAppDefinition(ctx: UserCtx) {
const layouts = await getLayouts() const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new roles.AccessController() const accessController = new roles.AccessController()
@ -231,7 +230,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
} }
} }
async function performAppCreate(ctx: BBContext) { async function performAppCreate(ctx: UserCtx) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[] const apps = (await dbCore.getAllApps({ dev: true })) as App[]
const name = ctx.request.body.name, const name = ctx.request.body.name,
possibleUrl = ctx.request.body.url possibleUrl = ctx.request.body.url
@ -360,7 +359,7 @@ async function creationEvents(request: any, app: App) {
} }
} }
async function appPostCreate(ctx: BBContext, app: App) { async function appPostCreate(ctx: UserCtx, app: App) {
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
await migrations.backPopulateMigrations({ await migrations.backPopulateMigrations({
type: MigrationType.APP, type: MigrationType.APP,
@ -378,7 +377,7 @@ async function appPostCreate(ctx: BBContext, app: App) {
return quotas.addRows(rowCount) return quotas.addRows(rowCount)
}) })
} catch (err: any) { } catch (err: any) {
if (err.code && err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { if (err.code && err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
// this import resulted in row usage exceeding the quota // this import resulted in row usage exceeding the quota
// delete the app // delete the app
// skip pre and post-steps as no rows have been added to quotas yet // skip pre and post-steps as no rows have been added to quotas yet
@ -391,7 +390,7 @@ async function appPostCreate(ctx: BBContext, app: App) {
} }
} }
export async function create(ctx: BBContext) { export async function create(ctx: UserCtx) {
const newApplication = await quotas.addApp(() => performAppCreate(ctx)) const newApplication = await quotas.addApp(() => performAppCreate(ctx))
await appPostCreate(ctx, newApplication) await appPostCreate(ctx, newApplication)
await cache.bustCache(cache.CacheKey.CHECKLIST) await cache.bustCache(cache.CacheKey.CHECKLIST)
@ -401,7 +400,7 @@ export async function create(ctx: BBContext) {
// This endpoint currently operates as a PATCH rather than a PUT // This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present // Thus name and url fields are handled only if present
export async function update(ctx: BBContext) { export async function update(ctx: UserCtx) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[] const apps = (await dbCore.getAllApps({ dev: true })) as App[]
// validation // validation
const name = ctx.request.body.name, const name = ctx.request.body.name,
@ -421,7 +420,7 @@ export async function update(ctx: BBContext) {
ctx.body = app ctx.body = app
} }
export async function updateClient(ctx: BBContext) { export async function updateClient(ctx: UserCtx) {
// Get current app version // Get current app version
const db = context.getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentType.APP_METADATA) const application = await db.get(DocumentType.APP_METADATA)
@ -445,7 +444,7 @@ export async function updateClient(ctx: BBContext) {
ctx.body = app ctx.body = app
} }
export async function revertClient(ctx: BBContext) { export async function revertClient(ctx: UserCtx) {
// Check app can be reverted // Check app can be reverted
const db = context.getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentType.APP_METADATA) const application = await db.get(DocumentType.APP_METADATA)
@ -471,7 +470,7 @@ export async function revertClient(ctx: BBContext) {
ctx.body = app ctx.body = app
} }
const unpublishApp = async (ctx: any) => { async function unpublishApp(ctx: UserCtx) {
let appId = ctx.params.appId let appId = ctx.params.appId
appId = dbCore.getProdAppID(appId) appId = dbCore.getProdAppID(appId)
@ -487,7 +486,7 @@ const unpublishApp = async (ctx: any) => {
return result return result
} }
async function destroyApp(ctx: BBContext) { async function destroyApp(ctx: UserCtx) {
let appId = ctx.params.appId let appId = ctx.params.appId
appId = dbCore.getProdAppID(appId) appId = dbCore.getProdAppID(appId)
const devAppId = dbCore.getDevAppID(appId) const devAppId = dbCore.getDevAppID(appId)
@ -515,12 +514,12 @@ async function destroyApp(ctx: BBContext) {
return result return result
} }
async function preDestroyApp(ctx: BBContext) { async function preDestroyApp(ctx: UserCtx) {
const { rows } = await getUniqueRows([ctx.params.appId]) const { rows } = await getUniqueRows([ctx.params.appId])
ctx.rowCount = rows.length ctx.rowCount = rows.length
} }
async function postDestroyApp(ctx: BBContext) { async function postDestroyApp(ctx: UserCtx) {
const rowCount = ctx.rowCount const rowCount = ctx.rowCount
await groups.cleanupApp(ctx.params.appId) await groups.cleanupApp(ctx.params.appId)
if (rowCount) { if (rowCount) {
@ -528,7 +527,7 @@ async function postDestroyApp(ctx: BBContext) {
} }
} }
export async function destroy(ctx: BBContext) { export async function destroy(ctx: UserCtx) {
await preDestroyApp(ctx) await preDestroyApp(ctx)
const result = await destroyApp(ctx) const result = await destroyApp(ctx)
await postDestroyApp(ctx) await postDestroyApp(ctx)
@ -536,7 +535,7 @@ export async function destroy(ctx: BBContext) {
ctx.body = result ctx.body = result
} }
export const unpublish = async (ctx: BBContext) => { export async function unpublish(ctx: UserCtx) {
const prodAppId = dbCore.getProdAppID(ctx.params.appId) const prodAppId = dbCore.getProdAppID(ctx.params.appId)
const dbExists = await dbCore.dbExists(prodAppId) const dbExists = await dbCore.dbExists(prodAppId)
@ -551,7 +550,7 @@ export const unpublish = async (ctx: BBContext) => {
ctx.status = 204 ctx.status = 204
} }
export async function sync(ctx: BBContext) { export async function sync(ctx: UserCtx) {
const appId = ctx.params.appId const appId = ctx.params.appId
try { try {
ctx.body = await sdk.applications.syncApp(appId) ctx.body = await sdk.applications.syncApp(appId)

Some files were not shown because too many files have changed in this diff Show more