This commit is contained in:
Martin McKeaveney 2023-07-06 21:58:19 +01:00
commit abce52d846
477 changed files with 12404 additions and 13448 deletions

View File

@ -10,4 +10,4 @@ packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
packages/builder/cypress/reports
packages/sdk/sdk
packages/sdk/sdk

View File

@ -5,7 +5,7 @@
"jest": true,
"node": true
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module",
@ -16,23 +16,26 @@
"dist",
"public",
"*.spec.js",
"bundle.js",
"packages/pro"
"bundle.js"
],
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],
"overrides": [
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
"files": ["**/*.svelte"],
"extends": "plugin:svelte/recommended",
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": 2019,
"sourceType": "module",
"allowImportExportEverywhere": true
}
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": [],
"extends": [
"eslint:recommended"
],
"extends": ["eslint:recommended"],
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",
@ -44,7 +47,8 @@
}
],
"rules": {
"no-self-assign": "off"
"no-self-assign": "off",
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }]
},
"globals": {
"GeolocationPositionError": true

View File

@ -1,5 +1,9 @@
name: Budibase CI
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on:
# Trigger the workflow on push or pull request,
# but only for the master branch
@ -22,7 +26,16 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -34,10 +47,16 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -52,10 +71,16 @@ jobs:
test-libraries:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -72,10 +97,16 @@ jobs:
test-services:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -91,11 +122,14 @@ jobs:
test-pro:
runs-on: ubuntu-latest
if: github.repository == github.event.pull_request.head.repo.full_name
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -107,10 +141,16 @@ jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.repository == github.event.pull_request.head.repo.full_name
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.repository != github.event.pull_request.head.repo.full_name
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
@ -129,21 +169,47 @@ jobs:
check-pro-submodule:
runs-on: ubuntu-latest
if: github.repository == github.event.pull_request.head.repo.full_name
steps:
- name: Checkout code
- name: Checkout repo and submodules
uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Check submodule
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Check pro commit
id: get_pro_commits
run: |
cd packages/pro
git fetch
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
echo "Current commit has not been merged to develop"
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
exit 1
pro_commit=$(git rev-parse HEAD)
branch="${{ github.base_ref || github.ref_name }}"
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
if [[ $branch == "master" ]]; then
base_commit=$(git rev-parse origin/master)
else
echo "All good, the submodule had been merged!"
base_commit=$(git rev-parse origin/develop)
fi
echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit"
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
- name: Check submodule merged to develop
uses: actions/github-script@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
if (submoduleCommit !== baseCommit) {
console.error('Submodule commit does not match the latest commit on the develop branch.');
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
process.exit(1);
} else {
console.log('All good, the submodule had been merged and setup correctly!')
}

View File

@ -12,31 +12,22 @@ jobs:
runs-on: ubuntu-latest
steps:
# - name: Fail if not a tag
# run: |
# if [[ $GITHUB_REF != refs/tags/* ]]; then
# echo "Workflow Dispatch can only be run on tags"
# exit 1
# fi
- uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: Fail if tag is not in master
# run: |
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
# exit 1
# fi
- name: Pull values.yaml from budibase-infra
- name: Fail if not a tag
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.production.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
wc -l values.production.yaml
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Get the latest budibase release version
id: version
@ -48,29 +39,10 @@ jobs:
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Deploy to EKS
uses: craftech-io/eks-helm-deploy-action@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
cluster-name: budibase-eks-production
config-files: values.production.yaml
chart-path: charts/budibase
namespace: budibase
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
name: budibase-prod
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}
repository: budibase/budibase-deploys
event: budicloud-prod-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -24,51 +24,18 @@ jobs:
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Pull values.yaml from budibase-infra
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.preprod.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
wc -l values.preprod.yaml
- name: Deploy to Preprod Environment
uses: budibase/helm@v1.8.0
with:
release: budibase-preprod
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
helm: helm3
values: |
globals:
appVersion: v${{ env.RELEASE_VERSION }}
ingress:
enabled: true
nginx: true
value-files: >-
[
"values.preprod.yaml"
]
- uses: passeidireto/trigger-external-workflow-action@main
env:
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
embed-title: ${{ env.RELEASE_VERSION }}
repository: budibase/budibase-deploys
event: budicloud-preprod-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -34,7 +34,6 @@ jobs:
exit 1
fi
- uses: actions/setup-node@v1
with:
node-version: 14.x
@ -58,9 +57,12 @@ jobs:
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: "Get Current tag"
id: currenttag
run: |
version=v$(./scripts/getCurrentVersion.sh)
echo 'Using tag $version'
echo "::set-output name=tag::$resversionult"
- name: Build/release Docker images
run: |
@ -69,7 +71,7 @@ jobs:
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
release-helm-chart:
needs: [release-images]

View File

@ -32,10 +32,11 @@ jobs:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- run: cd scripts && yarn
- name: Tag prerelease
run: |
cd scripts
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./scripts/versionCommit.sh prerelease
./versionCommit.sh prerelease

View File

@ -42,12 +42,13 @@ jobs:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- run: cd scripts && yarn
- name: Tag release
run: |
cd scripts
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./scripts/versionCommit.sh $BUMP_TYPE
./versionCommit.sh $BUMP_TYPE

3
babel.config.json Normal file
View File

@ -0,0 +1,3 @@
{
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
}

View File

@ -126,6 +126,16 @@ http {
proxy_pass http://app-service;
}
location /embed {
rewrite /embed/(.*) /app/$1 break;
proxy_pass http://app-service;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header x-budibase-embed "true";
add_header x-budibase-embed "true";
add_header Content-Security-Policy "frame-ancestors *";
}
location /builder {
proxy_read_timeout 120s;
proxy_connect_timeout 120s;

View File

@ -92,6 +92,16 @@ http {
proxy_pass $apps;
}
location /embed {
rewrite /embed/(.*) /app/$1 break;
proxy_pass $apps;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header x-budibase-embed "true";
add_header x-budibase-embed "true";
add_header Content-Security-Policy "frame-ancestors *";
}
location = / {
proxy_pass $apps;
}

View File

@ -2,7 +2,9 @@ const fs = require("fs")
const { execSync } = require("child_process")
const path = require("path")
const IMAGES = {
const IS_SINGLE_IMAGE = process.env.SINGLE_IMAGE
let IMAGES = {
worker: "budibase/worker",
apps: "budibase/apps",
proxy: "budibase/proxy",
@ -10,7 +12,13 @@ const IMAGES = {
couch: "ibmcom/couchdb3",
curl: "curlimages/curl",
redis: "redis",
watchtower: "containrrr/watchtower"
watchtower: "containrrr/watchtower",
}
if (IS_SINGLE_IMAGE) {
IMAGES = {
budibase: "budibase/budibase"
}
}
const FILES = {
@ -39,11 +47,10 @@ for (let image in IMAGES) {
}
// copy config files
copyFile(FILES.COMPOSE)
if (!IS_SINGLE_IMAGE) {
copyFile(FILES.COMPOSE)
}
copyFile(FILES.ENV)
// compress
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
// clean up
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)

View File

@ -37,6 +37,14 @@ COPY --from=build /worker /worker
RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
# Install postgres client for pg_dump utils
RUN apt install software-properties-common apt-transport-https gpg -y \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https gpg -y
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \

View File

@ -1,22 +1,10 @@
{
"version": "2.7.7-alpha.0",
"version": "2.8.2-alpha.3",
"npmClient": "yarn",
"packages": [
"packages/backend-core",
"packages/bbui",
"packages/builder",
"packages/cli",
"packages/client",
"packages/frontend-core",
"packages/sdk",
"packages/server",
"packages/shared-core",
"packages/string-templates",
"packages/types",
"packages/worker",
"packages/pro/packages/pro"
"packages/*"
],
"useWorkspaces": true,
"useNx": true,
"command": {
"publish": {
"ignoreChanges": [

View File

@ -2,30 +2,31 @@
"name": "root",
"private": true,
"devDependencies": {
"@esbuild-plugins/node-resolve": "^0.2.2",
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.2.1",
"@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0",
"babel-eslint": "^10.0.3",
"esbuild": "^0.17.18",
"eslint": "^7.28.0",
"esbuild-node-externals": "^1.7.0",
"eslint": "^8.44.0",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-svelte3": "^3.2.0",
"husky": "^8.0.3",
"js-yaml": "^4.1.0",
"kill-port": "^1.6.1",
"lerna": "7.0.0-alpha.0",
"lerna": "7.0.2",
"madge": "^6.0.0",
"minimist": "^1.2.8",
"nx": "^16.2.1",
"prettier": "^2.3.1",
"prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"semver": "^7.5.0",
"svelte": "^3.38.2",
"typescript": "4.7.3"
"typescript": "4.7.3",
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"eslint-plugin-svelte": "^2.32.2",
"svelte-eslint-parser": "^0.32.0"
},
"scripts": {
"preinstall": "node scripts/syncProPackage.js",
@ -48,16 +49,16 @@
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && yarn build --projects=@budibase/client && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages && eslint qa-core",
"lint:eslint": "eslint packages qa-core --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs",
@ -67,6 +68,7 @@
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
@ -95,19 +97,7 @@
},
"workspaces": {
"packages": [
"packages/backend-core",
"packages/bbui",
"packages/builder",
"packages/cli",
"packages/client",
"packages/frontend-core",
"packages/sdk",
"packages/server",
"packages/shared-core",
"packages/string-templates",
"packages/types",
"packages/worker",
"packages/pro/packages/pro"
"packages/*"
]
},
"resolutions": {
@ -116,5 +106,6 @@
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0"
},
"dependencies": {}
"dependencies": {
}
}

View File

@ -31,4 +31,6 @@ const config: Config.InitialOptions = {
coverageReporters: ["lcov", "json", "clover"],
}
process.env.DISABLE_PINO_LOGGER = "1"
export default config

View File

@ -27,7 +27,7 @@
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",
"aws-sdk": "2.1030.0",
"bcrypt": "5.0.1",
"bcrypt": "5.1.0",
"bcryptjs": "2.4.3",
"bull": "4.10.1",
"correlation-id": "4.0.0",

View File

@ -5,6 +5,7 @@ import {
GoogleInnerConfig,
OIDCConfig,
OIDCInnerConfig,
OIDCLogosConfig,
SCIMConfig,
SCIMInnerConfig,
SettingsConfig,
@ -191,6 +192,10 @@ export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
// OIDC
export async function getOIDCLogosDoc(): Promise<OIDCLogosConfig | undefined> {
return getConfig<OIDCLogosConfig>(ConfigType.OIDC_LOGOS)
}
async function getOIDCConfigDoc(): Promise<OIDCConfig | undefined> {
return getConfig<OIDCConfig>(ConfigType.OIDC)
}

View File

@ -57,6 +57,9 @@ class Replication {
appReplicateOpts() {
return {
filter: (doc: any) => {
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
return false
}
return doc._id !== DocumentType.APP_METADATA
},
}

View File

@ -0,0 +1,14 @@
export function checkErrorCode(error: any, code: number) {
const stringCode = code.toString()
if (typeof error === "object") {
return error.status === code || error.message?.includes(stringCode)
} else if (typeof error === "number") {
return error === code
} else if (typeof error === "string") {
return error.includes(stringCode)
}
}
export function isDocumentConflictError(error: any) {
return checkErrorCode(error, 409)
}

View File

@ -9,3 +9,4 @@ export * from "../constants/db"
export { getGlobalDBName, baseGlobalDBName } from "../context"
export * from "./lucene"
export * as searchIndexes from "./searchIndexes"
export * from "./errors"

View File

@ -343,6 +343,9 @@ export class QueryBuilder<T> {
}
const oneOf = (key: string, value: any) => {
if (!value) {
return `*:*`
}
if (!Array.isArray(value)) {
if (typeof value === "string") {
value = value.split(",")

View File

@ -114,6 +114,25 @@ describe("lucene", () => {
expect(resp.rows.length).toBe(2)
})
it("should return all rows when doing a one of search against falsey value", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addOneOf("property", null)
let resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", undefined)
resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", "")
resp = await builder.run()
expect(resp.rows.length).toBe(3)
builder.addOneOf("property", [])
resp = await builder.run()
expect(resp.rows.length).toBe(0)
})
it("should be able to perform a contains search", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addContains("property", ["word"])

View File

@ -81,8 +81,19 @@ export function generateAppUserID(prodAppId: string, userId: string) {
* Generates a new role ID.
* @returns {string} The new role ID which the role doc can be stored under.
*/
export function generateRoleID(id?: any) {
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
export function generateRoleID(name: string) {
const prefix = `${DocumentType.ROLE}${SEPARATOR}`
if (name.startsWith(prefix)) {
return name
}
return `${prefix}${name}`
}
/**
* Utility function to be more verbose.
*/
export function prefixRoleID(name: string) {
return generateRoleID(name)
}
/**

View File

@ -14,10 +14,15 @@ async function servedBuilder(timezone: string) {
await publishEvent(Event.SERVED_BUILDER, properties)
}
async function servedApp(app: App, timezone: string) {
async function servedApp(
app: App,
timezone: string,
embed?: boolean | undefined
) {
const properties: AppServedEvent = {
appVersion: app.version,
timezone,
embed: embed === true,
}
await publishEvent(Event.SERVED_APP, properties)
}

View File

@ -21,6 +21,6 @@ export function logAlertWithInfo(
logAlert(message, error)
}
export function logWarn(message: string) {
console.warn(`bb-warn: ${message}`)
export function logWarn(message: string, e?: any) {
console.warn(`bb-warn: ${message}`, e)
}

View File

@ -1,10 +1,11 @@
import * as google from "../sso/google"
import { Cookie } from "../../../constants"
import { clearCookie, getCookie } from "../../../utils"
import * as configs from "../../../configs"
import { BBContext, SSOProfile } from "@budibase/types"
import * as cache from "../../../cache"
import * as utils from "../../../utils"
import { UserCtx, SSOProfile } from "@budibase/types"
import { ssoSaveUserNoOp } from "../sso/sso"
import { cache, utils } from "../../../"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
type Passport = {
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
export async function preAuth(
passport: Passport,
ctx: BBContext,
ctx: UserCtx,
next: Function
) {
// get the relevant config
@ -49,7 +50,7 @@ export async function preAuth(
export async function postAuth(
passport: Passport,
ctx: BBContext,
ctx: UserCtx,
next: Function
) {
// get the relevant config
@ -57,7 +58,7 @@ export async function postAuth(
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
return passport.authenticate(
new GoogleStrategy(
@ -72,7 +73,7 @@ export async function postAuth(
_profile: SSOProfile,
done: Function
) => {
clearCookie(ctx, Cookie.DatasourceAuth)
utils.clearCookie(ctx, Cookie.DatasourceAuth)
done(null, { accessToken, refreshToken })
}
),

View File

@ -1,12 +1,17 @@
import crypto from "crypto"
import fs from "fs"
import zlib from "zlib"
import env from "../environment"
import { join } from "path"
const ALGO = "aes-256-ctr"
const SEPARATOR = "-"
const ITERATIONS = 10000
const RANDOM_BYTES = 16
const STRETCH_LENGTH = 32
const SALT_LENGTH = 16
const IV_LENGTH = 16
export enum SecretOption {
API = "api",
ENCRYPTION = "encryption",
@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string {
return secret
}
function stretchString(string: string, salt: Buffer) {
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
function stretchString(secret: string, salt: Buffer) {
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
}
export function encrypt(
input: string,
secretOption: SecretOption = SecretOption.API
) {
const salt = crypto.randomBytes(RANDOM_BYTES)
const salt = crypto.randomBytes(SALT_LENGTH)
const stretched = stretchString(getSecret(secretOption), salt)
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
const base = cipher.update(input)
@ -60,3 +65,115 @@ export function decrypt(
const final = decipher.final()
return Buffer.concat([base, final]).toString()
}
export async function encryptFile(
{ dir, filename }: { dir: string; filename: string },
secret: string
) {
const outputFileName = `${filename}.enc`
const filePath = join(dir, filename)
const inputFile = fs.createReadStream(filePath)
const outputFile = fs.createWriteStream(join(dir, outputFileName))
const salt = crypto.randomBytes(SALT_LENGTH)
const iv = crypto.randomBytes(IV_LENGTH)
const stretched = stretchString(secret, salt)
const cipher = crypto.createCipheriv(ALGO, stretched, iv)
outputFile.write(salt)
outputFile.write(iv)
inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile)
return new Promise<{ filename: string; dir: string }>(r => {
outputFile.on("finish", () => {
r({
filename: outputFileName,
dir,
})
})
})
}
async function getSaltAndIV(path: string) {
const fileStream = fs.createReadStream(path)
const salt = await readBytes(fileStream, SALT_LENGTH)
const iv = await readBytes(fileStream, IV_LENGTH)
fileStream.close()
return { salt, iv }
}
export async function decryptFile(
inputPath: string,
outputPath: string,
secret: string
) {
const { salt, iv } = await getSaltAndIV(inputPath)
const inputFile = fs.createReadStream(inputPath, {
start: SALT_LENGTH + IV_LENGTH,
})
const outputFile = fs.createWriteStream(outputPath)
const stretched = stretchString(secret, salt)
const decipher = crypto.createDecipheriv(ALGO, stretched, iv)
const unzip = zlib.createGunzip()
inputFile.pipe(decipher).pipe(unzip).pipe(outputFile)
return new Promise<void>((res, rej) => {
outputFile.on("finish", () => {
outputFile.close()
res()
})
inputFile.on("error", e => {
outputFile.close()
rej(e)
})
decipher.on("error", e => {
outputFile.close()
rej(e)
})
unzip.on("error", e => {
outputFile.close()
rej(e)
})
outputFile.on("error", e => {
outputFile.close()
rej(e)
})
})
}
function readBytes(stream: fs.ReadStream, length: number) {
return new Promise<Buffer>((resolve, reject) => {
let bytesRead = 0
const data: Buffer[] = []
stream.on("readable", () => {
let chunk
while ((chunk = stream.read(length - bytesRead)) !== null) {
data.push(chunk)
bytesRead += chunk.length
}
resolve(Buffer.concat(data))
})
stream.on("end", () => {
reject(new Error("Insufficient data in the stream."))
})
stream.on("error", error => {
reject(error)
})
})
}

View File

@ -1,5 +1,5 @@
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { generateRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
import { getAppDB } from "../context"
import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types"
@ -25,18 +25,28 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [
BUILTIN_IDS.PUBLIC,
]
export const RoleIDVersion = {
// original version, with a UUID based ID
UUID: undefined,
// new version - with name based ID
NAME: "name",
}
export class Role implements RoleDoc {
_id: string
_rev?: string
name: string
permissionId: string
inherits?: string
version?: string
permissions = {}
constructor(id: string, name: string, permissionId: string) {
this._id = id
this.name = name
this.permissionId = permissionId
// version for managing the ID - removing the role_ when responding
this.version = RoleIDVersion.NAME
}
addInheritance(inherits: string) {
@ -140,9 +150,13 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
* to check if the role inherits any others.
* @param {string|null} roleId The level ID to lookup.
* @param {object|null} opts options for the function, like whether to halt errors, instead return public.
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
*/
export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
export async function getRole(
roleId?: string,
opts?: { defaultPublic?: boolean }
): Promise<RoleDoc | undefined> {
if (!roleId) {
return undefined
}
@ -153,14 +167,20 @@ export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
role = cloneDeep(
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
)
} else {
// make sure has the prefix (if it has it then it won't be added)
roleId = prefixRoleID(roleId)
}
try {
const db = getAppDB()
const dbRole = await db.get(getDBRoleID(roleId))
role = Object.assign(role, dbRole)
// finalise the ID
role._id = getExternalRoleID(role._id)
role._id = getExternalRoleID(role._id, role.version)
} catch (err) {
if (!isBuiltin(roleId) && opts?.defaultPublic) {
return cloneDeep(BUILTIN_ROLES.PUBLIC)
}
// only throw an error if there is no role at all
if (Object.keys(role).length === 0) {
throw err
@ -254,6 +274,9 @@ export async function getAllRoles(appId?: string) {
})
)
roles = body.rows.map((row: any) => row.doc)
roles.forEach(
role => (role._id = getExternalRoleID(role._id!, role.version))
)
}
const builtinRoles = getBuiltinRoles()
@ -261,14 +284,15 @@ export async function getAllRoles(appId?: string) {
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter(
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
dbRole =>
getExternalRoleID(dbRole._id!, dbRole.version) === builtinRoleId
)[0]
if (dbBuiltin == null) {
roles.push(builtinRole || builtinRoles.BASIC)
} else {
// remove role and all back after combining with the builtin
roles = roles.filter(role => role._id !== dbBuiltin._id)
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
dbBuiltin._id = getExternalRoleID(dbBuiltin._id!, dbBuiltin.version)
roles.push(Object.assign(builtinRole, dbBuiltin))
}
}
@ -374,19 +398,22 @@ export class AccessController {
/**
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
*/
export function getDBRoleID(roleId?: string) {
if (roleId?.startsWith(DocumentType.ROLE)) {
return roleId
export function getDBRoleID(roleName: string) {
if (roleName?.startsWith(DocumentType.ROLE)) {
return roleName
}
return generateRoleID(roleId)
return prefixRoleID(roleName)
}
/**
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
*/
export function getExternalRoleID(roleId?: string) {
export function getExternalRoleID(roleId: string, version?: string) {
// for built-in roles we want to remove the DB role ID element (role_)
if (roleId?.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) {
if (
(roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) ||
version === RoleIDVersion.NAME
) {
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
}
return roleId

View File

@ -16,8 +16,6 @@
export let tooltip = undefined
export let newStyles = true
export let id
let showTooltip = false
</script>
<button
@ -35,9 +33,6 @@
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
{#if icon}
<svg
@ -52,19 +47,7 @@
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
</svg>
</div>
{/if}
{#if showTooltip && tooltip}
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
@ -75,7 +58,6 @@
button {
position: relative;
}
.spectrum-Button-label {
white-space: nowrap;
overflow: hidden;
@ -93,11 +75,13 @@
text-align: center;
transform: translateX(-50%);
left: 50%;
top: calc(100% - 3px);
top: 100%;
opacity: 0;
transition: opacity 130ms ease-out;
pointer-events: none;
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
button:hover .tooltip {
opacity: 1;
}
.spectrum-Button--primary.new-styles {
background: var(--spectrum-global-color-gray-800);

View File

@ -15,8 +15,6 @@
const dispatch = createEventDispatcher()
$: placeholder = !value
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]

View File

@ -8,6 +8,8 @@
export let disabled = false
export let error = null
export let validate = null
export let indeterminate = false
export let compact = false
const dispatch = createEventDispatcher()
@ -21,11 +23,19 @@
}
</script>
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
<FancyField
{error}
{value}
{validate}
{disabled}
{compact}
clickable
on:click={onChange}
>
<span>
<Checkbox {disabled} {value} />
<Checkbox {disabled} {value} {indeterminate} />
</span>
<div class="text">
<div class="text" class:compact>
{#if text}
{text}
{/if}
@ -47,6 +57,10 @@
line-clamp: 2;
-webkit-box-orient: vertical;
}
.text.compact {
font-size: 13px;
line-height: 15px;
}
.text > :global(*) {
font-size: inherit !important;
}

View File

@ -0,0 +1,69 @@
<script>
import FancyCheckbox from "./FancyCheckbox.svelte"
import FancyForm from "./FancyForm.svelte"
import { createEventDispatcher } from "svelte"
export let options = []
export let selected = []
export let showSelectAll = true
export let selectAllText = "Select all"
let selectedBooleans = options.map(x => selected.indexOf(x) > -1)
const dispatch = createEventDispatcher()
$: updateSelected(selectedBooleans)
$: allSelected = selected?.length === options.length
$: noneSelected = !selected?.length
function reset() {
return Array(options.length).fill(true)
}
function updateSelected(selectedArr) {
const array = []
for (let [i, isSelected] of Object.entries(selectedArr)) {
if (isSelected) {
array.push(options[i])
}
}
selected = array
dispatch("change", selected)
}
function toggleSelectAll() {
if (allSelected === true) {
selectedBooleans = []
} else {
selectedBooleans = reset()
}
dispatch("change", selected)
}
</script>
{#if options && Array.isArray(options)}
<div class="checkbox-group" class:has-select-all={showSelectAll}>
<FancyForm on:change>
{#if showSelectAll}
<FancyCheckbox
bind:value={allSelected}
on:change={toggleSelectAll}
text={selectAllText}
indeterminate={!allSelected && !noneSelected}
compact
/>
{/if}
{#each options as option, i}
<FancyCheckbox bind:value={selectedBooleans[i]} text={option} compact />
{/each}
</FancyForm>
</div>
{/if}
<style>
.checkbox-group.has-select-all :global(.fancy-field:first-of-type) {
background: var(--spectrum-global-color-gray-100);
}
.checkbox-group.has-select-all :global(.fancy-field:first-of-type:hover) {
background: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -11,6 +11,7 @@
export let value
export let ref
export let autoHeight
export let compact = false
const formContext = getContext("fancy-form")
const id = Math.random()
@ -42,6 +43,7 @@
class:disabled
class:focused
class:clickable
class:compact
class:auto-height={autoHeight}
>
<div class="content" on:click>
@ -61,7 +63,6 @@
<style>
.fancy-field {
max-width: 400px;
background: var(--spectrum-global-color-gray-75);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
@ -69,6 +70,12 @@
transition: border-color 130ms ease-out, background 130ms ease-out,
background 130ms ease-out;
color: var(--spectrum-global-color-gray-800);
--padding: 16px;
--height: 64px;
}
.fancy-field.compact {
--padding: 8px;
--height: 36px;
}
.fancy-field:hover {
border-color: var(--spectrum-global-color-gray-400);
@ -91,8 +98,8 @@
}
.content {
position: relative;
height: 64px;
padding: 0 16px;
height: var(--height);
padding: 0 var(--padding);
}
.fancy-field.auto-height .content {
height: auto;
@ -103,7 +110,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 16px;
gap: var(--padding);
}
.field {
flex: 1 1 auto;

View File

@ -4,4 +4,5 @@ export { default as FancySelect } from "./FancySelect.svelte"
export { default as FancyButton } from "./FancyButton.svelte"
export { default as FancyForm } from "./FancyForm.svelte"
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
export { default as FancyCheckboxGroup } from "./FancyCheckboxGroup.svelte"
export { default as ErrorMessage } from "./ErrorMessage.svelte"

View File

@ -9,6 +9,7 @@
export let text = null
export let disabled = false
export let size
export let indeterminate = false
const dispatch = createEventDispatcher()
const onChange = event => {
@ -22,6 +23,7 @@
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error}
class:checked={value}
class:is-indeterminate={indeterminate}
>
<input
checked={value}

View File

@ -71,6 +71,7 @@
timeOnly,
enableTime,
time24hr,
disabled,
}
const handleChange = event => {

View File

@ -150,7 +150,7 @@
</div>
{:else if variables.length}
<div style="max-height: 100px">
{#each variables as variable, idx}
{#each variables as variable}
<li
class="spectrum-Menu-item"
role="option"

View File

@ -99,9 +99,15 @@
bind:this={button}
>
{#if fieldIcon}
<span class="option-extra icon">
<Icon size="S" name={fieldIcon} />
</span>
{#if !useOptionIconImage}
<span class="option-extra icon">
<Icon size="S" name={fieldIcon} />
</span>
{:else}
<span class="option-extra icon field-icon">
<img src={fieldIcon} alt="icon" width="15" height="15" />
</span>
{/if}
{/if}
{#if fieldColour}
<span class="option-extra">
@ -311,4 +317,8 @@
max-width: 170px;
font-size: 12px;
}
.option-extra.icon.field-icon {
display: flex;
}
</style>

View File

@ -1,6 +1,7 @@
<script>
import "@spectrum-css/link/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let href = "#"
export let size = "M"
@ -10,18 +11,61 @@
export let overBackground = false
export let target
export let download
export let disabled = false
export let tooltip = null
const dispatch = createEventDispatcher()
const onClick = e => {
if (!disabled) {
dispatch("click")
e.stopPropagation()
}
}
</script>
<a
on:click={e => dispatch("click") && e.stopPropagation()}
on:click={onClick}
{href}
{target}
{download}
class:disabled
class:spectrum-Link--primary={primary}
class:spectrum-Link--secondary={secondary}
class:spectrum-Link--overBackground={overBackground}
class:spectrum-Link--quiet={quiet}
class="spectrum-Link spectrum-Link--size{size}"><slot /></a
class="spectrum-Link spectrum-Link--size{size}"
>
<slot />
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping direction="bottom" text={tooltip} />
</div>
{/if}
</a>
<style>
a {
position: relative;
}
a.disabled {
color: var(--spectrum-global-color-gray-500);
}
a.disabled:hover {
text-decoration: none;
cursor: default;
}
.tooltip {
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
opacity: 0;
transition: 130ms ease-out;
pointer-events: none;
z-index: 100;
}
a:hover .tooltip {
opacity: 1;
}
</style>

View File

@ -87,7 +87,7 @@
border-color: var(--spectrum-global-color-gray-400);
}
/* Toolbar button color */
:global(.EasyMDEContainer .editor-toolbar button i) {
:global(.EasyMDEContainer .editor-toolbar button) {
color: var(--spectrum-global-color-gray-800);
}
/* Separator between toolbar buttons*/

View File

@ -8,6 +8,7 @@
export let fixed = false
export let inline = false
export let disableCancel = false
const dispatch = createEventDispatcher()
let visible = fixed || inline
@ -38,7 +39,7 @@
}
export function cancel() {
if (!visible) {
if (!visible || disableCancel) {
return
}
dispatch("cancel")

View File

@ -1,3 +1,7 @@
<script context="module">
export const keepOpen = Symbol("keepOpen")
</script>
<script>
import "@spectrum-css/dialog/dist/index-vars.css"
import { getContext } from "svelte"
@ -30,7 +34,7 @@
async function secondary(e) {
loading = true
if (!secondaryAction || (await secondaryAction(e)) !== false) {
if (!secondaryAction || (await secondaryAction(e)) !== keepOpen) {
hide()
}
loading = false
@ -38,7 +42,7 @@
async function confirm() {
loading = true
if (!onConfirm || (await onConfirm()) !== false) {
if (!onConfirm || (await onConfirm()) !== keepOpen) {
hide()
}
loading = false
@ -46,7 +50,7 @@
async function close() {
loading = true
if (!onCancel || (await onCancel()) !== false) {
if (!onCancel || (await onCancel()) !== keepOpen) {
cancel()
}
loading = false

View File

@ -90,6 +90,6 @@
.spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);
overflow: auto;
overflow: visible;
}
</style>

View File

@ -29,7 +29,6 @@
$: type = getType(schema)
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
$: width = schema?.width || "150px"
$: cellValue = getCellValue(value, schema.template)
const getType = schema => {

View File

@ -204,6 +204,12 @@
})
return columns
.sort((a, b) => {
if (a.divider) {
return a
}
if (b.divider) {
return b
}
const orderA = a.order || Number.MAX_SAFE_INTEGER
const orderB = b.order || Number.MAX_SAFE_INTEGER
const nameA = getDisplayName(a)
@ -373,7 +379,7 @@
</div>
{/if}
{#if sortedRows?.length}
{#each sortedRows as row, idx}
{#each sortedRows as row}
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
{#if showEditColumn}
<div

View File

@ -42,7 +42,7 @@ export { default as MenuSection } from "./Menu/Section.svelte"
export { default as MenuSeparator } from "./Menu/Separator.svelte"
export { default as MenuItem } from "./Menu/Item.svelte"
export { default as Modal } from "./Modal/Modal.svelte"
export { default as ModalContent } from "./Modal/ModalContent.svelte"
export { default as ModalContent, keepOpen } from "./Modal/ModalContent.svelte"
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
export { default as Notification } from "./Notification/Notification.svelte"
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"

View File

@ -5,9 +5,10 @@
<meta charset='utf8'>
<meta name='viewport' content='width=device-width'>
<title>Budibase</title>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
rel="stylesheet" />
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
</head>
<body id="app">

View File

@ -9,7 +9,8 @@
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
"test": "vitest run"
"test": "vitest run",
"test:watch": "vitest"
},
"jest": {
"globals": {
@ -70,6 +71,7 @@
"@codemirror/state": "^6.2.0",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.11.2",
"@fontsource/source-sans-pro": "^5.0.3",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@ -122,6 +124,7 @@
"tsconfig-paths": "4.0.0",
"typescript": "4.7.3",
"vite": "^3.0.8",
"vite-plugin-static-copy": "^0.16.0",
"vitest": "^0.29.2"
},
"nx": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@ -1,46 +0,0 @@
import { datasources, tables } from "../stores/backend"
import { IntegrationNames } from "../constants/backend"
import { get } from "svelte/store"
import cloneDeep from "lodash/cloneDeepWith"
import { API } from "api"
function prepareData(config) {
let datasource = {}
let existingTypeCount = get(datasources).list.filter(
ds => ds.source === config.type
).length
let baseName = IntegrationNames[config.type] || config.name
let name =
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
datasource.type = "datasource"
datasource.source = config.type
datasource.config = config.config
datasource.name = name
datasource.plus = config.plus
return datasource
}
export async function saveDatasource(config, skipFetch = false) {
const datasource = prepareData(config)
// Create datasource
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
// update the tables incase datasource plus
await tables.fetch()
await datasources.select(resp._id)
return resp
}
export async function createRestDatasource(integration) {
const config = cloneDeep(integration)
return saveDatasource(config)
}
export async function validateDatasourceConfig(config) {
const datasource = prepareData(config)
const resp = await API.validateDatasource(datasource)
return resp
}

View File

@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments"
import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
export const userStore = getUserStore()
export const deploymentStore = getDeploymentStore()
// Setup history for screens
export const screenHistoryStore = createHistoryStore({
@ -118,3 +120,20 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
x => x._id === $automationStore.selectedAutomationId
)
})
// Derive map of resource IDs to other users.
// We only ever care about a single user in each resource, so if multiple users
// share the same datasource we can just overwrite them.
export const userSelectedResourceMap = derived(userStore, $userStore => {
let map = {}
$userStore.forEach(user => {
if (user.builderMetadata?.selectedResourceId) {
map[user.builderMetadata?.selectedResourceId] = user
}
})
return map
})
export const isOnlyUser = derived(userStore, $userStore => {
return $userStore.length === 1
})

View File

@ -0,0 +1,22 @@
import { writable } from "svelte/store"
import { API } from "api"
import { notifications } from "@budibase/bbui"
export const getDeploymentStore = () => {
let store = writable([])
const load = async () => {
try {
store.set(await API.getAppDeployments())
} catch (err) {
notifications.error("Error fetching deployments")
}
}
return {
subscribe: store.subscribe,
actions: {
load,
},
}
}

View File

@ -38,6 +38,7 @@ import {
import { makePropSafe as safe } from "@budibase/string-templates"
import { getComponentFieldOptions } from "helpers/formFields"
import { createBuilderWebsocket } from "builderStore/websocket"
import { BuilderSocketEvent } from "@budibase/shared-core"
const INITIAL_FRONTEND_STATE = {
initialised: false,
@ -61,6 +62,9 @@ const INITIAL_FRONTEND_STATE = {
showNotificationAction: false,
sidePanel: false,
},
features: {
componentValidation: false,
},
errors: [],
hasAppPackage: false,
libraries: null,
@ -74,6 +78,7 @@ const INITIAL_FRONTEND_STATE = {
propertyFocus: null,
builderSidePanel: false,
hasLock: true,
showPreview: false,
// URL params
selectedScreenId: null,
@ -116,10 +121,13 @@ export const getFrontendStore = () => {
reset: () => {
store.set({ ...INITIAL_FRONTEND_STATE })
websocket?.disconnect()
websocket = null
},
initialise: async pkg => {
const { layouts, screens, application, clientLibPath, hasLock } = pkg
websocket = createBuilderWebsocket(application.appId)
if (!websocket) {
websocket = createBuilderWebsocket(application.appId)
}
await store.actions.components.refreshDefinitions(application.appId)
// Reset store state
@ -144,6 +152,11 @@ export const getFrontendStore = () => {
navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [],
hasLock,
features: {
...INITIAL_FRONTEND_STATE.features,
...application.features,
},
icon: application.icon || {},
initialised: true,
}))
screenHistoryStore.reset()
@ -224,6 +237,7 @@ export const getFrontendStore = () => {
legalDirectChildren = []
) => {
const type = component._component
if (illegalChildren.includes(type)) {
return type
}
@ -237,10 +251,13 @@ export const getFrontendStore = () => {
return
}
if (type === "@budibase/standard-components/sidepanel") {
illegalChildren = []
}
const definition = store.actions.components.getDefinition(
component._component
)
// Reset whitelist for direct children
legalDirectChildren = []
if (definition?.legalDirectChildren?.length) {
@ -279,9 +296,12 @@ export const getFrontendStore = () => {
}
},
save: async screen => {
// Validate screen structure
// Temporarily disabled to accommodate migration issues
// store.actions.screens.validate(screen)
const state = get(store)
// Validate screen structure if the app supports it
if (state.features?.componentValidation) {
store.actions.screens.validate(screen)
}
// Check screen definition for any component settings which need updated
store.actions.screens.enrichEmptySettings(screen)
@ -292,7 +312,6 @@ export const getFrontendStore = () => {
const routesResponse = await API.fetchAppRoutes()
// If plugins changed we need to fetch the latest app metadata
const state = get(store)
let usedPlugins = state.usedPlugins
if (savedScreen.pluginAdded) {
const { application } = await API.fetchAppPackage(state.appId)
@ -335,6 +354,33 @@ export const getFrontendStore = () => {
}
return await sequentialScreenPatch(patchFn, screenId)
},
replace: async (screenId, screen) => {
if (!screenId) {
return
}
if (!screen) {
// Screen deletion
store.update(state => ({
...state,
screens: state.screens.filter(x => x._id !== screenId),
}))
} else {
const index = get(store).screens.findIndex(x => x._id === screen._id)
if (index === -1) {
// Screen addition
store.update(state => ({
...state,
screens: [...state.screens, screen],
}))
} else {
// Screen update
store.update(state => {
state.screens[index] = screen
return state
})
}
}
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
@ -1287,7 +1333,7 @@ export const getFrontendStore = () => {
links: {
save: async (url, title) => {
const navigation = get(store).navigation
let links = [...navigation?.links]
let links = [...(navigation?.links ?? [])]
// Skip if we have an identical link
if (links.find(link => link.url === url && link.text === title)) {
@ -1347,6 +1393,21 @@ export const getFrontendStore = () => {
})
},
},
websocket: {
selectResource: id => {
websocket.emit(BuilderSocketEvent.SelectResource, {
resourceId: id,
})
},
},
metadata: {
replace: metadata => {
store.update(state => ({
...state,
...metadata,
}))
},
},
}
return store

View File

@ -1,10 +1,12 @@
import { createWebsocket } from "@budibase/frontend-core"
import { userStore, store } from "builderStore"
import { userStore, store, deploymentStore } from "builderStore"
import { datasources, tables } from "stores/backend"
import { get } from "svelte/store"
import { auth } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder")
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
})
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
if (userId === get(auth)?.user?._id) {
notifications.success("You can now edit screens and automations")
store.update(state => ({
...state,
hasLock: true,
@ -39,15 +40,32 @@ export const createBuilderWebsocket = appId => {
}
})
// Table events
// Data section events
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
tables.replaceTable(id, table)
})
// Datasource events
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
// Design section events
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
store.actions.screens.replace(id, screen)
})
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
store.actions.metadata.replace(metadata)
})
socket.onOther(
BuilderSocketEvent.AppPublishChange,
async ({ user, published }) => {
await apps.load()
if (published) {
await deploymentStore.actions.load()
}
const verb = published ? "published" : "unpublished"
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
}
)
return socket
}

View File

@ -12,7 +12,7 @@
import { automationStore, selectedAutomation } from "builderStore"
import { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions"
import { TriggerStepID } from "constants/backend/automations"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { checkForCollectStep } from "builderStore/utils"
export let blockIdx
@ -149,7 +149,7 @@
<div class="item-body">
<Icon name={action.icon} />
<Body size="XS">{action.name}</Body>
{#if isDisabled && !syncAutomationsEnabled}
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
<div class="tag-color">
<Tags>
<Tag icon="LockClosed">Business</Tag>
@ -168,7 +168,7 @@
<Layout noPadding gap="XS">
<Detail size="S">Plugins</Detail>
<div class="item-list">
{#each Object.entries(plugins) as [idx, action]}
{#each Object.entries(plugins) as [_, action]}
<div
class="item"
class:selected={selectedAction === action.name}

View File

@ -60,6 +60,7 @@
</script>
<div>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html html}
</div>

View File

@ -78,9 +78,6 @@
}
async function removeLooping() {
let loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
try {
await automationStore.actions.deleteAutomationBlock(loopBlock)
} catch (error) {
@ -89,10 +86,6 @@
}
async function deleteStep() {
let loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
try {
if (loopBlock) {
await automationStore.actions.deleteAutomationBlock(loopBlock)
@ -168,8 +161,8 @@
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
)}
block={loopBlock}
{webhookModal}
block={loopBlock}
/>
</Layout>
</div>
@ -191,7 +184,7 @@
{#if !isTrigger}
<div>
<div class="block-options">
{#if block?.features?.[Features.LOOPING] || !block.features}
{#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
<ActionButton on:click={() => addLooping()} icon="Reuse">
Add Looping
</ActionButton>

View File

@ -71,7 +71,7 @@
<Layout noPadding gap="XS">
<Label size="S">Trigger</Label>
<div class="item-list">
{#each triggers as [idx, trigger]}
{#each triggers as [_, trigger]}
<div
class="item"
class:selected={selectedTrigger === trigger.name}

View File

@ -32,7 +32,12 @@
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Duplicate" on:click={duplicateAutomation}>Duplicate</MenuItem>
<MenuItem
icon="Duplicate"
on:click={duplicateAutomation}
disabled={automation.definition.trigger.name === "Webhook"}
>Duplicate</MenuItem
>
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>

View File

@ -13,6 +13,8 @@
Modal,
notifications,
Icon,
Checkbox,
DatePicker,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation } from "builderStore"
@ -306,6 +308,11 @@
drawer.hide()
}
function canShowField(key, value) {
const dependsOn = value?.dependsOn
return !dependsOn || !!inputData[dependsOn]
}
onMount(async () => {
try {
await environment.loadVariables()
@ -317,210 +324,233 @@
<div class="fields">
{#each deprecatedSchemaProperties as [key, value]}
<div class="block-field">
{#if key !== "fields"}
<Label
tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
>
{/if}
{#if value.type === "string" && value.enum}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key)
}}
/>
{:else if value.customType === "column"}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
options={Object.keys(table?.schema || {})}
/>
{:else if value.customType === "filters"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type="email"
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={false}
updateOnChange={false}
drawerLeft="260px"
/>
{#if canShowField(key, value)}
<div class="block-field">
{#if key !== "fields" && value.type !== "boolean"}
<Label
tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder on:change={e => onChange(e, key)} value={inputData[key]} />
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
{block}
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
{:else if value.customType === "code"}
<CodeEditorModal>
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={[
jsAutocomplete([
...bindingsToCompletions(bindings, EditorModes.JS),
]),
]}
mode={EditorModes.JS}
height={500}
/>
<div class="messaging">
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>Add available bindings by typing <strong>$</strong></div>
</div>
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
{#if value.type === "string" && value.enum && canShowField(key, value)}
<Select
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else}
<div class="test">
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key)
}}
/>
{:else if value.type === "boolean"}
<div style="margin-top: 10px">
<Checkbox
text={value.title}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
</div>
{:else if value.type === "date"}
<DatePicker
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "column"}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
options={Object.keys(table?.schema || {})}
/>
{:else if value.customType === "filters"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
fillWidth={true}
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
type="email"
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={false}
updateOnChange={false}
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
drawerLeft="260px"
/>
</div>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
{block}
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={[
jsAutocomplete([
...bindingsToCompletions(bindings, EditorModes.JS),
]),
]}
mode={EditorModes.JS}
height={500}
/>
<div class="messaging">
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>Add available bindings by typing <strong>$</strong></div>
</div>
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
/>
{:else}
<div class="test">
<DrawerBindableInput
fillWidth={true}
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px"
/>
</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}
{/each}
</div>
<Modal bind:this={webhookModal} width="30%">

View File

@ -1,5 +1,5 @@
<script>
import { tables } from "stores/backend"
import { datasources, tables } from "stores/backend"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core"
@ -26,19 +26,33 @@
$: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.type !== "external"
const handleGridTableUpdate = async e => {
tables.replaceTable(id, e.detail)
// We need to refresh datasources when an external table changes.
// Type "external" may exist - sometimes type is "table" and sometimes it
// is "external" - it has different meanings in different endpoints.
// If we check both these then we hopefully catch all external tables.
if (e.detail?.type === "external" || e.detail?.sql) {
await datasources.fetch()
}
}
</script>
<div class="wrapper">
<Grid
{API}
tableId={id}
tableType={$tables.selected?.type}
allowAddRows={!isUsersTable}
allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false}
on:updatetable={e => tables.replaceTable(id, e.detail)}
on:updatetable={handleGridTableUpdate}
>
<svelte:fragment slot="filter">
<GridFilterButton />
</svelte:fragment>
<svelte:fragment slot="controls">
{#if isInternal}
<GridCreateViewButton />
@ -53,7 +67,6 @@
<GridImportButton />
{/if}
<GridExportButton />
<GridFilterButton />
<GridAddColumnModal />
<GridEditColumnModal />
{#if isUsersTable}

View File

@ -3,8 +3,6 @@
import { goto, params } from "@roxi/routify"
import { Table, Heading, Layout } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import {
TableNames,
UNEDITABLE_USER_FIELDS,
@ -33,7 +31,6 @@
$: selectedRows, dispatch("selectionUpdated", selectedRows)
$: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows()
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
$: {
UNSORTABLE_TYPES.forEach(type => {
Object.values(schema || {}).forEach(col => {

View File

@ -1,6 +1,6 @@
<script>
import { ActionButton, Modal, notifications } from "@budibase/bbui"
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
import { ActionButton, notifications } from "@budibase/bbui"
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
import { datasources } from "../../../../stores/backend"
import { createEventDispatcher } from "svelte"
@ -8,9 +8,7 @@
const dispatch = createEventDispatcher()
$: datasource = findDatasource(table?._id)
$: plusTables = datasource?.plus
? Object.values(datasource?.entities || {})
: []
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
let modal
@ -24,31 +22,32 @@
})
}
async function saveRelationship() {
try {
// Create datasource
await datasources.save(datasource)
notifications.success(`Relationship information saved.`)
dispatch("updatecolumns")
} catch (err) {
notifications.error(`Error saving relationship info: ${err}`)
}
const afterSave = ({ action }) => {
notifications.success(`Relationship ${action} successfully`)
dispatch("updatecolumns")
}
const onError = err => {
notifications.error(`Error saving relationship info: ${err}`)
}
</script>
{#if datasource}
<div>
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
<ActionButton
icon="DataCorrelated"
primary
quiet
on:click={() => modal.show({ fromTable: table })}
>
Define relationship
</ActionButton>
</div>
<Modal bind:this={modal}>
<CreateEditRelationship
{datasource}
save={saveRelationship}
close={modal.hide}
{plusTables}
selectedFromTable={table}
/>
</Modal>
<CreateEditRelationshipModal
bind:this={modal}
{datasource}
{tables}
{afterSave}
{onError}
/>
{/if}

View File

@ -14,6 +14,12 @@
$: tempValue = filters || []
$: schemaFields = Object.values(schema || {})
$: text = getText(filters)
const getText = filters => {
const count = filters?.length
return count ? `Filter (${count})` : "Filter"
}
</script>
<ActionButton
@ -23,7 +29,7 @@
on:click={modal.show}
selected={tempValue?.length > 0}
>
Filter
{text}
</ActionButton>
<Modal bind:this={modal}>
<ModalContent

View File

@ -4,6 +4,9 @@
const { columns, tableId, filter, table } = getContext("grid")
// Wipe filter whenever table ID changes to avoid using stale filters
$: $tableId, filter.set([])
const onFilter = e => {
filter.set(e.detail || [])
}

View File

@ -4,12 +4,12 @@
export let disabled = false
const { rows, tableId, tableType } = getContext("grid")
const { rows, tableId, table } = getContext("grid")
</script>
<ImportButton
{disabled}
tableId={$tableId}
{tableType}
tableType={$table?.type}
on:importrows={rows.actions.refreshData}
/>

View File

@ -76,6 +76,10 @@ export function getBindings({
// will be replaced by the main array binding
readableBinding: label,
runtimeBinding: binding,
display: {
name: label,
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
},
})
}
return bindings

View File

@ -57,7 +57,6 @@
let table = $tables.selected
let confirmDeleteDialog
let deletion
let savingColumn
let deleteColName
let jsonSchemaModal
@ -182,6 +181,15 @@
indexes,
})
dispatch("updatecolumns")
if (
saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
) {
// Fetching the new tables
tables.fetch()
// Fetching the new relationships
datasources.fetch()
}
if (originalName) {
notifications.success("Column updated successfully")
} else {
@ -206,7 +214,6 @@
notifications.success(`Column ${editableColumn.name} deleted`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
@ -257,13 +264,11 @@
function confirmDelete() {
confirmDeleteDialog.show()
deletion = true
}
function hideDeleteDialog() {
confirmDeleteDialog.hide()
deleteColName = ""
deletion = false
}
function getRelationshipOptions(field) {

View File

@ -1,10 +1,9 @@
<script>
import { createEventDispatcher } from "svelte"
import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api"
import { ModalContent } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
const FORMULA_TYPE = FIELDS.FORMULA.type
@ -41,8 +40,8 @@
} else {
notifications.error(`Failed to save row - ${error.message}`)
}
// Prevent modal closing if there were errors
return false
return keepOpen
}
}
</script>

View File

@ -5,7 +5,7 @@
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api"
import { ModalContent, Select, Link } from "@budibase/bbui"
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify"
@ -51,7 +51,7 @@
errors = [...errors, { message: "Role is required" }]
}
if (errors.length) {
return false
return keepOpen
}
try {
@ -79,8 +79,8 @@
} else {
notifications.error("Error saving user")
}
// Prevent closing the modal on errors
return false
return keepOpen
}
}
</script>

View File

@ -1,5 +1,5 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { notifications } from "@budibase/bbui"
@ -12,15 +12,14 @@
let selectedRole = BASE_ROLE
let errors = []
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
let validRegex = /^[a-zA-Z0-9_]*$/
// Don't allow editing of public role
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
$: selectedRoleId = selectedRole._id
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
$: isCreating = selectedRoleId == null || selectedRoleId === ""
$: hasUniqueRoleName = !otherRoles
?.map(role => role.name)
?.includes(selectedRole.name)
$: roleNameError = getRoleNameError(selectedRole.name)
$: valid =
selectedRole.name &&
@ -77,7 +76,7 @@
errors.push({ message: "Please choose permissions" })
}
if (errors.length) {
return false
return keepOpen
}
// Save/create the role
@ -85,8 +84,8 @@
await roles.save(selectedRole)
notifications.success("Role saved successfully")
} catch (error) {
notifications.error("Error saving role")
return false
notifications.error(`Error saving role - ${error.message}`)
return keepOpen
}
}
@ -97,7 +96,20 @@
changeRole()
notifications.success("Role deleted successfully")
} catch (error) {
notifications.error("Error deleting role")
notifications.error(`Error deleting role - ${error.message}`)
return false
}
}
const getRoleNameError = name => {
const hasUniqueRoleName = !otherRoles
?.map(role => role.name)
?.includes(name)
const invalidRoleName = !validRegex.test(name)
if (!hasUniqueRoleName) {
return "Select a unique role name."
} else if (invalidRoleName) {
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
}
}
@ -108,7 +120,7 @@
title="Edit Roles"
confirmText={isCreating ? "Create" : "Save"}
onConfirm={saveRole}
disabled={!valid || !hasUniqueRoleName}
disabled={!valid || roleNameError}
>
{#if errors.length}
<ErrorsBox {errors} />
@ -128,8 +140,8 @@
<Input
label="Name"
bind:value={selectedRole.name}
disabled={shouldDisableRoleInput}
error={!hasUniqueRoleName ? "Select a unique role name." : null}
disabled={!!selectedRoleId}
error={roleNameError}
/>
<Select
label="Inherits Role"

View File

@ -92,13 +92,20 @@
},
}
function downloadWithBlob(data, filename) {
download(new Blob([data], { type: "text/plain" }), filename)
}
async function exportView() {
try {
const data = await API.exportView({
viewName: view,
format: exportFormat,
})
download(data, `export.${exportFormat === "csv" ? "csv" : "json"}`)
downloadWithBlob(
data,
`export.${exportFormat === "csv" ? "csv" : "json"}`
)
} catch (error) {
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
}
@ -111,7 +118,7 @@
rows: selectedRows.map(row => row._id),
format: exportFormat,
})
download(data, `export.${exportFormat}`)
downloadWithBlob(data, `export.${exportFormat}`)
} else if (filters || sorting) {
let response
try {
@ -130,7 +137,7 @@
notifications.error("Export Failed")
}
if (response) {
download(response, `export.${exportFormat}`)
downloadWithBlob(response, `export.${exportFormat}`)
notifications.success("Export Successful")
}
} else {

View File

@ -14,6 +14,7 @@
export let tableId
export let tableType
let rows = []
let allValid = false
let displayColumn = null

View File

@ -12,8 +12,11 @@
customQueryText,
} from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants"
import { userSelectedResourceMap } from "builderStore"
let openDataSources = []
$: enrichedDataSources = enrichDatasources(
$datasources,
$params,
@ -71,6 +74,13 @@
$goto(`./datasource/${datasource._id}`)
}
const selectTable = tableId => {
tables.select(tableId)
if (!$isActive("./table/:tableId")) {
$goto(`./table/${tableId}`)
}
}
function closeNode(datasource) {
openDataSources = openDataSources.filter(id => datasource._id !== id)
}
@ -151,15 +161,24 @@
{#if $database?._id}
<div class="hierarchy-items-container">
{#each enrichedDataSources as datasource, idx}
<NavItem
icon="UserGroup"
text="Users"
selected={$isActive("./table/:tableId") &&
$tables.selected?._id === TableNames.USERS}
on:click={() => selectTable(TableNames.USERS)}
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
/>
{#each enrichedDataSources as datasource}
<NavItem
border={idx > 0}
border
text={datasource.name}
opened={datasource.open}
selected={$isActive("./datasource") && datasource.selected}
withArrow={true}
on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)}
selectedBy={$userSelectedResourceMap[datasource._id]}
>
<div class="datasource-icon" slot="icon">
<IntegrationIcon
@ -174,7 +193,7 @@
</NavItem>
{#if datasource.open}
<TableNavigator sourceId={datasource._id} />
<TableNavigator sourceId={datasource._id} {selectTable} />
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
<NavItem
indentLevel={1}
@ -185,6 +204,7 @@
selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id}
on:click={() => $goto(`./query/${query._id}`)}
selectedBy={$userSelectedResourceMap[query._id]}
>
<EditQueryPopover {query} />
</NavItem>
@ -196,7 +216,7 @@
<style>
.hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-xl));
margin: 0 calc(-1 * var(--spacing-l));
}
.datasource-icon {
display: grid;

View File

@ -1,219 +0,0 @@
<script>
import {
Label,
Input,
Layout,
Toggle,
Button,
TextArea,
Modal,
EnvDropdown,
Accordion,
notifications,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "helpers"
import { IntegrationTypes } from "constants/backend"
import { createValidationStore } from "helpers/validation/yup"
import { createEventDispatcher, onMount } from "svelte"
import { environment, licensing, auth } from "stores/portal"
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
export let datasource
export let schema
export let creating
let createVariableModal
let selectedKey
const validation = createValidationStore()
const dispatch = createEventDispatcher()
function filter([key, value]) {
if (!value) {
return false
}
return !(
(datasource.source === IntegrationTypes.REST &&
key === "defaultHeaders") ||
value.deprecated
)
}
$: config = datasource?.config
$: configKeys = Object.entries(schema || {})
.filter(el => filter(el))
.map(([key]) => key)
// setup the validation for each required field
$: configKeys.forEach(key => {
if (schema[key].required) {
validation.addValidatorType(key, schema[key].type, schema[key].required)
}
})
// run the validation whenever the config changes
$: validation.check(config)
// dispatch the validation result
$: dispatch(
"valid",
Object.values($validation.errors).filter(val => val != null).length === 0
)
let addButton
function getDisplayName(key, fieldKey) {
let name
if (fieldKey && schema[key]["fields"][fieldKey]?.display) {
name = schema[key]["fields"][fieldKey].display
} else if (fieldKey) {
name = fieldKey
} else if (schema[key]?.display) {
name = schema[key].display
} else {
name = key
}
return capitalise(name)
}
function getDisplayError(error, configKey) {
return error?.replace(
new RegExp(`${configKey}`, "i"),
getDisplayName(configKey)
)
}
function getFieldGroupKeys(fieldGroup) {
return Object.entries(schema[fieldGroup].fields || {})
.filter(el => filter(el))
.map(([key]) => key)
}
async function save(data) {
try {
await environment.createVariable(data)
config[selectedKey] = `{{ env.${data.name} }}`
createVariableModal.hide()
} catch (err) {
notifications.error(`Failed to create variable: ${err.message}`)
}
}
function showModal(configKey) {
selectedKey = configKey
createVariableModal.show()
}
async function handleUpgradePanel() {
await environment.upgradePanelOpened()
$licensing.goToUpgradePage()
}
onMount(async () => {
try {
await environment.loadVariables()
if ($auth.user) {
await licensing.init()
}
} catch (err) {
console.error(err)
}
})
</script>
<form>
<Layout noPadding gap="S">
{#if !creating}
<div class="form-row">
<Label>Name</Label>
<Input on:change bind:value={datasource.name} />
</div>
{/if}
{#each configKeys as configKey}
{#if schema[configKey].type === "object"}
<div class="form-row ssl">
<Label>{getDisplayName(configKey)}</Label>
<Button secondary thin outline on:click={addButton.addEntry()}
>Add</Button
>
</div>
<KeyValueBuilder
bind:this={addButton}
defaults={schema[configKey].default}
bind:object={config[configKey]}
on:change
noAddButton={true}
/>
{:else if schema[configKey].type === "boolean"}
<div class="form-row">
<Label>{getDisplayName(configKey)}</Label>
<Toggle text="" bind:value={config[configKey]} />
</div>
{:else if schema[configKey].type === "longForm"}
<div class="form-row">
<Label>{getDisplayName(configKey)}</Label>
<TextArea
type={schema[configKey].type}
on:change
bind:value={config[configKey]}
error={getDisplayError($validation.errors[configKey], configKey)}
/>
</div>
{:else if schema[configKey].type === "fieldGroup"}
<Accordion
itemName={configKey}
initialOpen={getFieldGroupKeys(configKey).some(
fieldKey => !!config[fieldKey]
)}
header={getDisplayName(configKey)}
>
<Layout gap="S">
{#each getFieldGroupKeys(configKey) as fieldKey}
<div class="form-row">
<Label>{getDisplayName(configKey, fieldKey)}</Label>
<Input
type={schema[configKey]["fields"][fieldKey]?.type}
on:change
bind:value={config[fieldKey]}
/>
</div>
{/each}
</Layout>
</Accordion>
{:else}
<div class="form-row">
<Label>{getDisplayName(configKey)}</Label>
<EnvDropdown
showModal={() => showModal(configKey)}
variables={$environment.variables}
type={configKey === "port" ? "string" : schema[configKey].type}
on:change
bind:value={config[configKey]}
error={getDisplayError($validation.errors[configKey], configKey)}
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
{handleUpgradePanel}
/>
</div>
{/if}
{/each}
</Layout>
</form>
<Modal bind:this={createVariableModal}>
<CreateEditVariableModal {save} />
</Modal>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
.form-row.ssl {
display: grid;
grid-template-columns: 20% 20%;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -1,249 +0,0 @@
<script>
import {
Heading,
Body,
Divider,
InlineAlert,
Button,
notifications,
Modal,
Table,
Toggle,
} from "@budibase/bbui"
import { datasources, integrations, tables } from "stores/backend"
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { goto } from "@roxi/routify"
import ValuesList from "components/common/ValuesList.svelte"
export let datasource
export let save
let tableSchema = {
name: {},
primary: { displayName: "Primary Key" },
}
let relationshipSchema = {
tables: {},
columns: {},
}
let relationshipModal
let createExternalTableModal
let selectedFromRelationship, selectedToRelationship
let confirmDialog
let specificTables = null
let requireSpecificTables = false
$: integration = datasource && $integrations[datasource.source]
$: plusTables = datasource?.plus
? Object.values(datasource?.entities || {})
: []
$: relationships = getRelationships(plusTables)
$: schemaError = $datasources.schemaError
$: relationshipInfo = relationshipTableData(relationships)
function getRelationships(tables) {
if (!tables || !Array.isArray(tables)) {
return {}
}
let pairs = {}
for (let table of tables) {
for (let column of Object.values(table.schema)) {
if (column.type !== "link") {
continue
}
// these relationships have an id to pair them to each other
// one has a main for the from side
const key = column.main ? "from" : "to"
pairs[column._id] = {
...pairs[column._id],
[key]: column,
}
}
}
return pairs
}
function buildRelationshipDisplayString(fromCol, toCol) {
function getTableName(tableId) {
if (!tableId || typeof tableId !== "string") {
return null
}
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
}
if (!toCol || !fromCol) {
return "Cannot build name"
}
const fromTableName = getTableName(toCol.tableId)
const toTableName = getTableName(fromCol.tableId)
const throughTableName = getTableName(fromCol.through)
let displayString
if (throughTableName) {
displayString = `${fromTableName} ↔ ${toTableName}`
} else {
displayString = `${fromTableName} → ${toTableName}`
}
return displayString
}
async function updateDatasourceSchema() {
try {
await datasources.updateSchema(datasource, specificTables)
notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch()
} catch (error) {
notifications.error(
`Error updating datasource schema ${
error?.message ? `: ${error.message}` : ""
}`
)
}
}
function onClickTable(table) {
$goto(`../../table/${table._id}`)
}
function openRelationshipModal(fromRelationship, toRelationship) {
selectedFromRelationship = fromRelationship || {}
selectedToRelationship = toRelationship || {}
relationshipModal.show()
}
function createNewTable() {
createExternalTableModal.show()
}
function relationshipTableData(relations) {
return Object.values(relations).map(relationship => ({
tables: buildRelationshipDisplayString(
relationship.from,
relationship.to
),
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
from: relationship.from,
to: relationship.to,
}))
}
</script>
<Modal bind:this={relationshipModal}>
<CreateEditRelationship
{datasource}
{save}
close={relationshipModal.hide}
{plusTables}
fromRelationship={selectedFromRelationship}
toRelationship={selectedToRelationship}
/>
</Modal>
<Modal bind:this={createExternalTableModal}>
<CreateExternalTableModal {datasource} />
</Modal>
<ConfirmDialog
bind:this={confirmDialog}
okText="Fetch tables"
onOk={updateDatasourceSchema}
onCancel={() => confirmDialog.hide()}
warning={false}
title="Confirm table fetch"
>
<Toggle
bind:value={requireSpecificTables}
on:change={e => {
requireSpecificTables = e.detail
specificTables = null
}}
thin
text="Fetch listed tables only (one per line)"
/>
{#if requireSpecificTables}
<ValuesList label="" bind:values={specificTables} />
{/if}
<br />
<Body>
If you have fetched tables from this database before, this action may
overwrite any changes you made after your initial fetch.
</Body>
</ConfirmDialog>
<Divider />
<div class="query-header">
<Heading size="S">Tables</Heading>
<div class="table-buttons">
<Button secondary on:click={() => confirmDialog.show()}>
Fetch tables
</Button>
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
</div>
</div>
<Body>
This datasource can determine tables automatically. Budibase can fetch your
tables directly from the database and you can use them without having to write
any queries at all.
</Body>
{#if schemaError}
<InlineAlert
type="error"
header="Error fetching tables"
message={schemaError}
onConfirm={datasources.removeSchemaError}
/>
{/if}
{#if plusTables && Object.values(plusTables).length > 0}
<Table
on:click={({ detail }) => onClickTable(detail)}
schema={tableSchema}
data={Object.values(plusTables)}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
/>
{:else}
<Body size="S"><i>No tables found.</i></Body>
{/if}
{#if integration.relationships !== false}
<Divider />
<div class="query-header">
<Heading size="S">Relationships</Heading>
<Button primary on:click={() => openRelationshipModal()}>
Define relationship
</Button>
</div>
<Body>
Tell budibase how your tables are related to get even more smart features.
</Body>
{#if relationshipInfo && relationshipInfo.length > 0}
<Table
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
schema={relationshipSchema}
data={relationshipInfo}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
{:else}
<Body size="S"><i>No relationships configured.</i></Body>
{/if}
{/if}
<style>
.query-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 0 0 var(--spacing-s) 0;
}
.table-buttons {
display: flex;
gap: var(--spacing-m);
}
</style>

View File

@ -1,86 +0,0 @@
<script>
import { Body, notifications } from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import ICONS from "../icons"
export let integration = {}
let integrations = []
const INTERNAL = "BUDIBASE"
async function fetchIntegrations() {
let otherIntegrations
try {
otherIntegrations = await API.getIntegrations()
} catch (error) {
otherIntegrations = {}
notifications.error("Error getting integrations")
}
integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...otherIntegrations,
}
}
function selectIntegration(integrationType) {
const selected = integrations[integrationType]
// build the schema
const schema = {}
for (let key of Object.keys(selected.datasource)) {
schema[key] = selected.datasource[key].default
}
integration = {
type: integrationType,
plus: selected.plus,
...schema,
}
}
onMount(() => {
fetchIntegrations()
})
</script>
<section>
<div class="integration-list">
{#each Object.entries(integrations) as [integrationType, schema]}
<div
class="integration hoverable"
class:selected={integration.type === integrationType}
on:click={() => selectIntegration(integrationType)}
>
<svelte:component
this={ICONS[integrationType]}
height="50"
width="50"
/>
<Body size="XS">{schema.name || integrationType}</Body>
</div>
{/each}
</div>
</section>
<style>
.integration-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-gap: var(--spectrum-alias-grid-baseline);
}
.integration {
display: grid;
background: var(--background-alt);
place-items: center;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
transition: 0.3s all;
border-radius: var(--spectrum-alias-item-rounded-border-radius-s);
}
.integration:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
</style>

View File

@ -1,123 +0,0 @@
<script>
import {
Divider,
Heading,
ActionButton,
Badge,
Body,
Layout,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
import {
getRestBindings,
getEnvironmentBindings,
readableToRuntimeBinding,
runtimeToReadableMap,
} from "builderStore/dataBinding"
import { cloneDeep } from "lodash/fp"
import { licensing } from "stores/portal"
export let datasource
export let queries
let addHeader
let parsedHeaders = runtimeToReadableMap(
getRestBindings(),
cloneDeep(datasource?.config?.defaultHeaders)
)
const onDefaultHeaderUpdate = headers => {
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
return acc
}, {})
datasource.config.defaultHeaders = flatHeaders
}
</script>
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Headers</Heading>
<Badge quiet grey>Optional</Badge>
</div>
</div>
<Body size="S">
Headers enable you to provide additional information about the request, such
as format.
</Body>
<KeyValueBuilder
bind:this={addHeader}
bind:object={parsedHeaders}
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
noAddButton
bindings={getRestBindings()}
/>
<div>
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
Add header
</ActionButton>
</div>
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Authentication</Heading>
<Badge quiet grey>Optional</Badge>
</div>
</div>
<Body size="S">
Create an authentication config that can be shared with queries.
</Body>
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Variables</Heading>
<Badge quiet grey>Optional</Badge>
</div>
</div>
<Body size="S"
>Variables enable you to store and re-use values in queries, with the choice
of a static value such as a token using static variables, or a value from a
query response using dynamic variables.</Body
>
<Heading size="XS">Static</Heading>
<Layout noPadding gap="XS">
<KeyValueBuilder
name="Variable"
keyPlaceholder="Name"
headings
bind:object={datasource.config.staticVariables}
on:change
bindings={$licensing.environmentVariablesEnabled
? getEnvironmentBindings()
: []}
/>
</Layout>
<div />
<Heading size="XS">Dynamic</Heading>
<Body size="S">
Dynamic variables are evaluated when a dependant query is executed. The value
is cached for a period of time and will be refreshed if a query fails.
</Body>
<ViewDynamicVariables {queries} {datasource} />
<style>
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.badge {
display: flex;
gap: var(--spacing-m);
}
</style>

View File

@ -1,60 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import { Heading, Detail } from "@budibase/bbui"
import IntegrationIcon from "../IntegrationIcon.svelte"
export let integration
export let integrationType
export let schema
let dispatcher = createEventDispatcher()
</script>
<div
class:selected={integration.type === integrationType}
on:click={() => dispatcher("selected", integrationType)}
class="item hoverable"
>
<div class="item-body" class:with-type={!!schema.type}>
<IntegrationIcon {integrationType} {schema} size="25" />
<div class="text">
<Heading size="XXS">{schema.friendlyName}</Heading>
{#if schema.type}
<Detail size="S">{schema.type || ""}</Detail>
{/if}
</div>
</div>
</div>
<style>
.item {
cursor: pointer;
display: grid;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s)
var(--spectrum-alias-item-padding-m);
background: var(--spectrum-alias-background-color-secondary);
transition: background 0.13s ease-out;
border-radius: 5px;
box-sizing: border-box;
border-width: 2px;
}
.item:hover,
.item.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.item-body {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
.item-body.with-type {
align-items: flex-start;
}
.item-body.with-type :global(svg) {
margin-top: 4px;
}
</style>

View File

@ -1,145 +0,0 @@
<script>
export let width = 100
export let height = 100
</script>
<svg
{width}
{height}
viewBox="0 0 46 46"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
>
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
<title>btn_google_dark_normal_ios</title>
<desc>Created with Sketch.</desc>
<defs>
<filter
x="-50%"
y="-50%"
width="200%"
height="200%"
filterUnits="objectBoundingBox"
id="filter-1"
>
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
<feGaussianBlur
stdDeviation="0.5"
in="shadowOffsetOuter1"
result="shadowBlurOuter1"
/>
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
in="shadowBlurOuter1"
type="matrix"
result="shadowMatrixOuter1"
/>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
<feGaussianBlur
stdDeviation="0.5"
in="shadowOffsetOuter2"
result="shadowBlurOuter2"
/>
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
in="shadowBlurOuter2"
type="matrix"
result="shadowMatrixOuter2"
/>
<feMerge>
<feMergeNode in="shadowMatrixOuter1" />
<feMergeNode in="shadowMatrixOuter2" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
</defs>
<g
id="Google-Button"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
sketch:type="MSPage"
>
<g
id="9-PATCH"
sketch:type="MSArtboardGroup"
transform="translate(-608.000000, -219.000000)"
/>
<g
id="btn_google_dark_normal"
sketch:type="MSArtboardGroup"
transform="translate(-1.000000, -1.000000)"
>
<g
id="button"
sketch:type="MSLayerGroup"
transform="translate(4.000000, 4.000000)"
filter="url(#filter-1)"
>
<g id="button-bg">
<use
fill="#4285F4"
fill-rule="evenodd"
sketch:type="MSShapeGroup"
xlink:href="#path-2"
/>
<use fill="none" xlink:href="#path-2" />
<use fill="none" xlink:href="#path-2" />
<use fill="none" xlink:href="#path-2" />
</g>
</g>
<g id="button-bg-copy">
<use
fill="#FFFFFF"
fill-rule="evenodd"
sketch:type="MSShapeGroup"
xlink:href="#path-3"
/>
<use fill="none" xlink:href="#path-3" />
<use fill="none" xlink:href="#path-3" />
<use fill="none" xlink:href="#path-3" />
</g>
<g
id="logo_googleg_48dp"
sketch:type="MSLayerGroup"
transform="translate(15.000000, 15.000000)"
>
<path
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
id="Shape"
fill="#4285F4"
sketch:type="MSShapeGroup"
/>
<path
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
id="Shape"
fill="#34A853"
sketch:type="MSShapeGroup"
/>
<path
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
id="Shape"
fill="#FBBC05"
sketch:type="MSShapeGroup"
/>
<path
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
id="Shape"
fill="#EA4335"
sketch:type="MSShapeGroup"
/>
<path
d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"
id="Shape"
sketch:type="MSShapeGroup"
/>
</g>
<g id="handles_square" sketch:type="MSLayerGroup" />
</g>
</g>
</svg>

View File

@ -31,65 +31,65 @@
<path
class="st1"
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
/>
<g>
<g>
<path
class="st0"
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"
/>
</g>
<g>
<path
class="st0"
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"
/>
</g>
</g>
<path
class="st2"
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
/>
<g>
<g>
<path
class="st1"
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"
/>
</g>
<g>
<path
class="st1"
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
/>
</g>
</g>
@ -102,24 +102,24 @@
<path
class="st1"
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
/>
</g>
<g>
<path
class="st1"
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
/>
</g>
</g>

View File

@ -44,6 +44,9 @@ export default ICONS
export function getIcon(integrationType, schema) {
const integrationList = get(integrations)
if (!integrationList) {
return
}
if (integrationList[integrationType]?.iconUrl) {
return { url: integrationList[integrationType].iconUrl }
} else if (schema?.custom || !ICONS[integrationType]) {

View File

@ -1,81 +0,0 @@
<script>
import { goto } from "@roxi/routify"
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import { IntegrationNames } from "constants/backend"
import cloneDeep from "lodash/cloneDeepWith"
import {
saveDatasource as save,
validateDatasourceConfig,
} from "builderStore/datasource"
import { DatasourceFeature } from "@budibase/types"
export let integration
// kill the reference so the input isn't saved
let datasource = cloneDeep(integration)
let isValid = false
$: name =
IntegrationNames[datasource.type] || datasource.name || datasource.type
async function validateConfig() {
const displayError = message =>
notifications.error(message ?? "Error validating datasource")
let connected = false
try {
const resp = await validateDatasourceConfig(datasource)
if (!resp.connected) {
displayError(`Unable to connect - ${resp.error}`)
}
connected = resp.connected
} catch (err) {
displayError(err?.message)
}
return connected
}
async function saveDatasource() {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig()
if (!valid) {
return false
}
}
try {
if (!datasource.name) {
datasource.name = name
}
const resp = await save(datasource)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource created successfully.`)
} catch (err) {
notifications.error(err?.message ?? "Error saving datasource")
// prevent the modal from closing
return false
}
}
</script>
<ModalContent
title={`Connect to ${name}`}
onConfirm={() => saveDatasource()}
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
cancelText="Back"
showSecondaryButton={datasource.plus}
size="L"
disabled={!isValid}
>
<Layout noPadding>
<Body size="XS"
>Connect your database to Budibase using the config below.
</Body>
</Layout>
<IntegrationConfigForm
schema={datasource.schema}
bind:datasource
creating={true}
on:valid={e => (isValid = e.detail)}
/>
</ModalContent>

View File

@ -1,110 +0,0 @@
<script>
import {
ModalContent,
Body,
Layout,
Link,
notifications,
} from "@budibase/bbui"
import { IntegrationNames, IntegrationTypes } from "constants/backend"
import GoogleButton from "../_components/GoogleButton.svelte"
import { organisation } from "stores/portal"
import { onMount } from "svelte"
import { validateDatasourceConfig } from "builderStore/datasource"
import cloneDeep from "lodash/cloneDeepWith"
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
import { goto } from "@roxi/routify"
import { saveDatasource } from "builderStore/datasource"
import { DatasourceFeature } from "@budibase/types"
export let integration
export let continueSetupId = false
let datasource = cloneDeep(integration)
datasource.config.continueSetupId = continueSetupId
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
onMount(async () => {
await organisation.init()
})
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
export const GoogleDatasouceConfigStep = {
AUTH: "Auth",
SET_URL: "Set_url",
}
let step = continueSetupId
? GoogleDatasouceConfigStep.SET_URL
: GoogleDatasouceConfigStep.AUTH
let isValid = false
const modalConfig = {
[GoogleDatasouceConfigStep.AUTH]: {},
[GoogleDatasouceConfigStep.SET_URL]: {
confirmButtonText: "Connect",
onConfirm: async () => {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
const resp = await validateDatasourceConfig(datasource)
if (!resp.connected) {
notifications.error(`Unable to connect - ${resp.error}`)
return false
}
}
try {
const resp = await saveDatasource(datasource)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource created successfully.`)
} catch (err) {
notifications.error(err?.message ?? "Error saving datasource")
// prevent the modal from closing
return false
}
},
},
}
</script>
<ModalContent
title={`Connect to ${integrationName}`}
cancelText="Cancel"
size="L"
confirmText={modalConfig[step].confirmButtonText}
showConfirmButton={!!modalConfig[step].onConfirm}
onConfirm={modalConfig[step].onConfirm}
disabled={!isValid}
>
{#if step === GoogleDatasouceConfigStep.AUTH}
<!-- check true and false directly, don't render until flag is set -->
{#if isGoogleConfigured === true}
<Layout noPadding>
<Body size="S"
>Authenticate with your google account to use the {integrationName} integration.</Body
>
</Layout>
<GoogleButton samePage />
{:else if isGoogleConfigured === false}
<Body size="S"
>Google authentication is not enabled, please complete Google SSO
configuration.</Body
>
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
{/if}
{/if}
{#if step === GoogleDatasouceConfigStep.SET_URL}
<Layout noPadding no>
<Body size="S">Add the URL of the sheet you want to connect.</Body>
<IntegrationConfigForm
schema={datasource.schema}
bind:datasource
creating={true}
on:valid={e => (isValid = e.detail)}
/>
</Layout>
{/if}
</ModalContent>

View File

@ -1,6 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import {
keepOpen,
ModalContent,
notifications,
Body,
@ -70,10 +71,9 @@
}
notifications.success(`Imported successfully.`)
return true
} catch (error) {
notifications.error("Error importing queries")
return false
return keepOpen
}
}
</script>

View File

@ -1,7 +1,9 @@
<script>
import { datasources } from "stores/backend"
import { get } from "svelte/store"
import { datasources, integrations } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { Input, ModalContent, Modal } from "@budibase/bbui"
import { integrationForDatasource } from "stores/selectors"
let error = ""
let modal
@ -32,7 +34,10 @@
...datasource,
name,
}
await datasources.save(updatedDatasource)
await datasources.update({
datasource: updatedDatasource,
integration: integrationForDatasource(get(integrations), datasource),
})
notifications.success(`Datasource ${name} updated successfully.`)
hide()
}

View File

@ -0,0 +1,45 @@
<script>
import ObjectField from "./fields/Object.svelte"
import BooleanField from "./fields/Boolean.svelte"
import LongFormField from "./fields/LongForm.svelte"
import FieldGroupField from "./fields/FieldGroup.svelte"
import StringField from "./fields/String.svelte"
import SelectField from "./fields/Select.svelte"
export let type
export let value
export let error
export let name
export let config
export let showModal = () => {}
const selectComponent = type => {
if (type === "object") {
return ObjectField
} else if (type === "boolean") {
return BooleanField
} else if (type === "longForm") {
return LongFormField
} else if (type === "fieldGroup") {
return FieldGroupField
} else if (type === "select") {
return SelectField
} else {
return StringField
}
}
$: component = selectComponent(type)
</script>
<svelte:component
this={component}
{type}
{value}
{error}
{name}
{config}
{showModal}
on:blur
on:change
/>

View File

@ -0,0 +1,20 @@
<script>
import { Label, Toggle } from "@budibase/bbui"
export let value
export let name
</script>
<div class="form-row">
<Label>{name}</Label>
<Toggle on:blur on:change text="" {value} />
</div>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,37 @@
<script>
import { createEventDispatcher } from "svelte"
import { Layout, Accordion } from "@budibase/bbui"
import ConfigInput from "../ConfigInput.svelte"
export let value
export let name
export let config
let dispatch = createEventDispatcher()
const handleChange = (updatedFieldKey, updatedFieldValue) => {
const updatedValue = value.map(field => {
return {
key: field.key,
value: field.key === updatedFieldKey ? updatedFieldValue : field.value,
}
})
dispatch("change", updatedValue)
}
</script>
<Accordion
initialOpen={config?.openByDefault ||
Object.values(value).some(properties => !!properties.value)}
header={name}
>
<Layout gap="S">
{#each value as field}
<ConfigInput
{...field}
on:change={e => handleChange(field.key, e.detail)}
/>
{/each}
</Layout>
</Accordion>

View File

@ -0,0 +1,22 @@
<script>
import { Label, TextArea } from "@budibase/bbui"
export let type
export let name
export let value
export let error
</script>
<div class="form-row">
<Label>{name}</Label>
<TextArea on:blur on:change {type} {value} {error} />
</div>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,37 @@
<script>
import { Label, Button } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
export let name
export let value
let addButton
</script>
<div class="form-row ssl">
<Label>{name}</Label>
<Button secondary thin outline on:click={addButton.addEntry()}>Add</Button>
</div>
<KeyValueBuilder
on:change
on:blur
bind:this={addButton}
defaults={value}
noAddButton={true}
/>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
.form-row.ssl {
display: grid;
grid-template-columns: 20% 20%;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,30 @@
<script>
import { Label, Select } from "@budibase/bbui"
export let type
export let name
export let value
export let error
export let config
</script>
<div class="form-row">
<Label>{name}</Label>
<Select
on:blur
on:change
options={config.options}
{type}
value={value || undefined}
{error}
/>
</div>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,39 @@
<script>
import { Label, EnvDropdown } from "@budibase/bbui"
import { environment, licensing } from "stores/portal"
export let type
export let name
export let value
export let error
export let showModal = () => {}
async function handleUpgradePanel() {
await environment.upgradePanelOpened()
$licensing.goToUpgradePage()
}
</script>
<div class="form-row">
<Label>{name}</Label>
<EnvDropdown
on:change
on:blur
type={type === "port" ? "string" : type}
{value}
{error}
variables={$environment.variables}
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
{showModal}
{handleUpgradePanel}
/>
</div>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,107 @@
<script>
import {
keepOpen,
Modal,
notifications,
Body,
Layout,
ModalContent,
} from "@budibase/bbui"
import { processStringSync } from "@budibase/string-templates"
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
import ConfigInput from "./ConfigInput.svelte"
import { createValidatedConfigStore } from "./stores/validatedConfig"
import { createValidatedNameStore } from "./stores/validatedName"
import { get } from "svelte/store"
import { environment } from "stores/portal"
export let integration
export let config
export let onSubmit = () => {}
export let showNameField = false
export let nameFieldValue = ""
$: configStore = createValidatedConfigStore(integration, config)
$: nameStore = createValidatedNameStore(nameFieldValue, showNameField)
const handleConfirm = async () => {
configStore.markAllFieldsActive()
nameStore.markActive()
if ((await configStore.validate()) && (await nameStore.validate())) {
const { config } = get(configStore)
const { name } = get(nameStore)
return onSubmit({
config,
name,
})
}
return keepOpen
}
let createVariableModal
let configValueSetterCallback = () => {}
const showModal = setter => {
configValueSetterCallback = setter
createVariableModal.show()
}
async function saveVariable(data) {
try {
await environment.createVariable(data)
configValueSetterCallback(`{{ env.${data.name} }}`)
createVariableModal.hide()
} catch (err) {
notifications.error(`Failed to create variable: ${err.message}`)
}
}
</script>
<ModalContent
title={`Connect to ${integration.friendlyName}`}
onConfirm={handleConfirm}
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
cancelText="Back"
disabled={$configStore.preventSubmit || $nameStore.preventSubmit}
size="L"
>
<Layout noPadding>
<Body size="XS">
Connect your database to Budibase using the config below.
</Body>
</Layout>
{#if showNameField}
<ConfigInput
type="string"
value={$nameStore.name}
error={$nameStore.error}
name="Name"
showModal={() => showModal(nameStore.updateValue)}
on:blur={nameStore.markActive}
on:change={e => nameStore.updateValue(e.detail)}
/>
{/if}
{#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config }}
{#if hidden === undefined || !eval(processStringSync(hidden, $configStore.config))}
<ConfigInput
{type}
{value}
{error}
{name}
{config}
showModal={() =>
showModal(newValue => configStore.updateFieldValue(key, newValue))}
on:blur={() => configStore.markFieldActive(key)}
on:change={e => configStore.updateFieldValue(key, e.detail)}
/>
{/if}
{/each}
</ModalContent>
<Modal bind:this={createVariableModal}>
<CreateEditVariableModal save={saveVariable} />
</Modal>

View File

@ -0,0 +1,144 @@
import { derived, writable, get } from "svelte/store"
import { getValidatorFields } from "./validation"
import { capitalise } from "helpers"
import { notifications } from "@budibase/bbui"
import { object } from "yup"
export const createValidatedConfigStore = (integration, config) => {
const configStore = writable(config)
const allValidators = getValidatorFields(integration)
const selectedValidatorsStore = writable({})
const errorsStore = writable({})
const validate = async () => {
try {
await object()
.shape(get(selectedValidatorsStore))
.validate(get(configStore), { abortEarly: false })
errorsStore.set({})
return true
} catch (error) {
// Yup error
if (error.inner) {
const errors = {}
error.inner.forEach(innerError => {
errors[innerError.path] = capitalise(innerError.message)
})
errorsStore.set(errors)
} else {
// Non-yup error
notifications.error("Unexpected validation error")
}
return false
}
}
const updateFieldValue = (key, value) => {
configStore.update($configStore => {
const newStore = { ...$configStore }
if (integration.datasource[key].type === "fieldGroup") {
value.forEach(field => {
newStore[field.key] = field.value
})
if (!integration.datasource[key].config?.nestedFields) {
value.forEach(field => {
newStore[field.key] = field.value
})
} else {
newStore[key] = value.reduce(
(p, field) => ({
...p,
[field.key]: field.value,
}),
{}
)
}
} else {
newStore[key] = value
}
return newStore
})
validate()
}
const markAllFieldsActive = () => {
selectedValidatorsStore.set(allValidators)
validate()
}
const markFieldActive = key => {
selectedValidatorsStore.update($validatorsStore => ({
...$validatorsStore,
[key]: allValidators[key],
}))
validate()
}
const combined = derived(
[configStore, errorsStore, selectedValidatorsStore],
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
const validatedConfig = []
Object.entries(integration.datasource).forEach(([key, properties]) => {
if (integration.name === "REST" && key !== "rejectUnauthorized") {
return
}
const getValue = () => {
if (properties.type === "fieldGroup") {
return Object.entries(properties.fields).map(
([fieldKey, fieldProperties]) => {
return {
key: fieldKey,
name: capitalise(fieldProperties.display || fieldKey),
type: fieldProperties.type,
value: $configStore[fieldKey],
}
}
)
}
return $configStore[key]
}
validatedConfig.push({
key,
value: getValue(),
error: $errorsStore[key],
name: capitalise(properties.display || key),
type: properties.type,
hidden: properties.hidden,
config: properties.config,
})
})
const allFieldsActive =
Object.keys($selectedValidatorsStore).length ===
Object.keys(allValidators).length
const hasErrors = Object.keys($errorsStore).length > 0
return {
validatedConfig,
config: $configStore,
errors: $errorsStore,
preventSubmit: allFieldsActive && hasErrors,
}
}
)
return {
subscribe: combined.subscribe,
updateFieldValue,
markAllFieldsActive,
markFieldActive,
validate,
}
}

View File

@ -0,0 +1,53 @@
import { derived, get, writable } from "svelte/store"
import { capitalise } from "helpers"
import { string } from "yup"
export const createValidatedNameStore = (name, isVisible) => {
const nameStore = writable(name)
const isActiveStore = writable(false)
const errorStore = writable(null)
const validate = async () => {
if (!isVisible || !get(isActiveStore)) {
return true
}
try {
await string().required().validate(get(nameStore), { abortEarly: false })
errorStore.set(null)
return true
} catch (error) {
errorStore.set(capitalise(error.message))
return false
}
}
const updateValue = value => {
nameStore.set(value)
validate()
}
const markActive = () => {
isActiveStore.set(true)
validate()
}
const combined = derived(
[nameStore, errorStore, isActiveStore],
([$nameStore, $errorStore, $isActiveStore]) => ({
name: $nameStore,
error: $errorStore,
preventSubmit: $errorStore !== null && $isActiveStore,
})
)
return {
subscribe: combined.subscribe,
updateValue,
markActive,
validate,
}
}

View File

@ -0,0 +1,27 @@
import { string, number } from "yup"
const propertyValidator = type => {
if (type === "number") {
return number().nullable()
}
if (type === "email") {
return string().email().nullable()
}
return string().nullable()
}
export const getValidatorFields = integration => {
const validatorFields = {}
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
if (properties.required) {
validatorFields[key] = propertyValidator(properties.type).required()
} else {
validatorFields[key] = propertyValidator(properties.type).notRequired()
}
})
return validatorFields
}

View File

@ -1,6 +1,7 @@
<script>
import { RelationshipTypes } from "constants/backend"
import {
keepOpen,
Button,
Input,
ModalContent,
@ -59,7 +60,6 @@
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
$: toRelationship.relationshipType = fromRelationship?.relationshipType
function getTable(id) {
return plusTables.find(table => table._id === id)
@ -180,6 +180,16 @@
return getErrorCount(errors) === 0
}
function otherRelationshipType(type) {
if (type === RelationshipTypes.MANY_TO_ONE) {
return RelationshipTypes.ONE_TO_MANY
} else if (type === RelationshipTypes.ONE_TO_MANY) {
return RelationshipTypes.MANY_TO_ONE
} else if (type === RelationshipTypes.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY
}
}
function buildRelationships() {
const id = Helpers.uuid()
//Map temporary variables
@ -200,6 +210,7 @@
...toRelationship,
tableId: fromId,
name: fromColumn,
relationshipType: otherRelationshipType(relationshipType),
through: throughId,
type: "link",
_id: id,
@ -267,7 +278,7 @@
async function saveRelationship() {
if (!validate()) {
return false
return keepOpen
}
buildRelationships()
removeExistingRelationship()

View File

@ -0,0 +1,66 @@
<script>
import { Modal } from "@budibase/bbui"
import { get } from "svelte/store"
import CreateEditRelationship from "./CreateEditRelationship.svelte"
import { integrations, datasources } from "stores/backend"
import { integrationForDatasource } from "stores/selectors"
export let datasource
export let tables
export let beforeSave = async () => {}
export let afterSave = async () => {}
export let onError = async () => {}
let relationshipModal
let fromRelationship = {}
let toRelationship = {}
let fromTable = null
export function show({
fromRelationship: selectedFromRelationship = {},
toRelationship: selectedToRelationship = {},
fromTable: selectedFromTable = null,
}) {
fromRelationship = selectedFromRelationship
toRelationship = selectedToRelationship
fromTable = selectedFromTable
relationshipModal.show()
}
export function hide() {
relationshipModal.hide()
}
// action is one of 'created', 'updated' or 'deleted'
async function saveRelationship(action) {
try {
await beforeSave({ action, datasource })
const integration = integrationForDatasource(
get(integrations),
datasource
)
await datasources.update({ datasource, integration })
await afterSave({ datasource, action })
} catch (err) {
await onError({ err, datasource, action })
}
}
</script>
<Modal bind:this={relationshipModal}>
<CreateEditRelationship
save={saveRelationship}
close={relationshipModal.hide}
selectedFromTable={fromTable}
{datasource}
plusTables={tables}
{fromRelationship}
{toRelationship}
/>
</Modal>
<style>
</style>

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