commit
8d6c690cd4
|
@ -16,8 +16,7 @@
|
||||||
"dist",
|
"dist",
|
||||||
"public",
|
"public",
|
||||||
"*.spec.js",
|
"*.spec.js",
|
||||||
"bundle.js",
|
"bundle.js"
|
||||||
"packages/pro"
|
|
||||||
],
|
],
|
||||||
"plugins": ["svelte3"],
|
"plugins": ["svelte3"],
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
|
@ -30,9 +29,7 @@
|
||||||
"files": ["**/*.ts"],
|
"files": ["**/*.ts"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"extends": [
|
"extends": ["eslint:recommended"],
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-inner-declarations": "off",
|
"no-inner-declarations": "off",
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
name: Budibase CI
|
name: Budibase CI
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Trigger the workflow on push or pull request,
|
# Trigger the workflow on push or pull request,
|
||||||
# but only for the master branch
|
# but only for the master branch
|
||||||
|
@ -22,7 +26,16 @@ jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -34,10 +47,16 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
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
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -52,10 +71,16 @@ jobs:
|
||||||
test-libraries:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
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
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -72,10 +97,16 @@ jobs:
|
||||||
test-services:
|
test-services:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
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
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -91,11 +122,14 @@ jobs:
|
||||||
|
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout repo and submodules
|
||||||
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -107,10 +141,16 @@ jobs:
|
||||||
integration-test:
|
integration-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
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
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -129,21 +169,47 @@ jobs:
|
||||||
|
|
||||||
check-pro-submodule:
|
check-pro-submodule:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Check submodule
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
|
- name: Check pro commit
|
||||||
|
id: get_pro_commits
|
||||||
run: |
|
run: |
|
||||||
cd packages/pro
|
cd packages/pro
|
||||||
git fetch
|
pro_commit=$(git rev-parse HEAD)
|
||||||
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"
|
branch="${{ github.base_ref || github.ref_name }}"
|
||||||
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
|
echo "Running on branch `$branch` (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
||||||
exit 1
|
|
||||||
|
if [[ $branch == "master" ]]; then
|
||||||
|
base_commit=$(git rev-parse origin/master)
|
||||||
else
|
else
|
||||||
echo "All good, the submodule had been merged!"
|
base_commit=$(git rev-parse origin/develop)
|
||||||
fi
|
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!')
|
||||||
|
}
|
||||||
|
|
|
@ -32,10 +32,11 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- run: yarn
|
- run: cd scripts && yarn
|
||||||
- name: Tag prerelease
|
- name: Tag prerelease
|
||||||
run: |
|
run: |
|
||||||
|
cd scripts
|
||||||
# setup the username and email.
|
# setup the username and email.
|
||||||
git config --global user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config --global user.email "<>"
|
git config --global user.email "<>"
|
||||||
./scripts/versionCommit.sh prerelease
|
./versionCommit.sh prerelease
|
||||||
|
|
|
@ -42,12 +42,13 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- run: yarn
|
- run: cd scripts && yarn
|
||||||
- name: Tag release
|
- name: Tag release
|
||||||
run: |
|
run: |
|
||||||
|
cd scripts
|
||||||
# setup the username and email.
|
# setup the username and email.
|
||||||
git config --global user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config --global user.email "<>"
|
git config --global user.email "<>"
|
||||||
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
|
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
|
||||||
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
|
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
|
||||||
./scripts/versionCommit.sh $BUMP_TYPE
|
./versionCommit.sh $BUMP_TYPE
|
||||||
|
|
|
@ -126,6 +126,16 @@ http {
|
||||||
proxy_pass http://app-service;
|
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 {
|
location /builder {
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 120s;
|
||||||
proxy_connect_timeout 120s;
|
proxy_connect_timeout 120s;
|
||||||
|
|
|
@ -92,6 +92,16 @@ http {
|
||||||
proxy_pass $apps;
|
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 = / {
|
location = / {
|
||||||
proxy_pass $apps;
|
proxy_pass $apps;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ const fs = require("fs")
|
||||||
const { execSync } = require("child_process")
|
const { execSync } = require("child_process")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
|
||||||
const IMAGES = {
|
const IS_SINGLE_IMAGE = process.env.SINGLE_IMAGE
|
||||||
|
|
||||||
|
let IMAGES = {
|
||||||
worker: "budibase/worker",
|
worker: "budibase/worker",
|
||||||
apps: "budibase/apps",
|
apps: "budibase/apps",
|
||||||
proxy: "budibase/proxy",
|
proxy: "budibase/proxy",
|
||||||
|
@ -10,7 +12,13 @@ const IMAGES = {
|
||||||
couch: "ibmcom/couchdb3",
|
couch: "ibmcom/couchdb3",
|
||||||
curl: "curlimages/curl",
|
curl: "curlimages/curl",
|
||||||
redis: "redis",
|
redis: "redis",
|
||||||
watchtower: "containrrr/watchtower"
|
watchtower: "containrrr/watchtower",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_SINGLE_IMAGE) {
|
||||||
|
IMAGES = {
|
||||||
|
budibase: "budibase/budibase"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILES = {
|
const FILES = {
|
||||||
|
@ -39,11 +47,10 @@ for (let image in IMAGES) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy config files
|
// copy config files
|
||||||
copyFile(FILES.COMPOSE)
|
if (!IS_SINGLE_IMAGE) {
|
||||||
|
copyFile(FILES.COMPOSE)
|
||||||
|
}
|
||||||
copyFile(FILES.ENV)
|
copyFile(FILES.ENV)
|
||||||
|
|
||||||
// compress
|
// compress
|
||||||
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
|
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
|
||||||
|
|
||||||
// clean up
|
|
||||||
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
|
|
|
@ -37,6 +37,14 @@ COPY --from=build /worker /worker
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
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
|
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||||
WORKDIR /nodejs
|
WORKDIR /nodejs
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \
|
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \
|
||||||
|
|
18
lerna.json
18
lerna.json
|
@ -1,22 +1,10 @@
|
||||||
{
|
{
|
||||||
"version": "2.7.36",
|
"version": "2.7.37-alpha.14",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/*"
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"useWorkspaces": true,
|
"useNx": true,
|
||||||
"command": {
|
"command": {
|
||||||
"publish": {
|
"publish": {
|
||||||
"ignoreChanges": [
|
"ignoreChanges": [
|
||||||
|
|
29
package.json
29
package.json
|
@ -2,28 +2,26 @@
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-resolve": "^0.2.2",
|
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@nx/js": "16.2.1",
|
"@nx/js": "16.2.1",
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"esbuild": "^0.17.18",
|
"esbuild": "^0.17.18",
|
||||||
|
"esbuild-node-externals": "^1.7.0",
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-plugin-cypress": "^2.11.3",
|
"eslint-plugin-cypress": "^2.11.3",
|
||||||
"eslint-plugin-svelte3": "^3.2.0",
|
"eslint-plugin-svelte3": "^3.2.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.0.0-alpha.0",
|
"lerna": "7.0.2",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"nx": "^16.2.1",
|
"prettier": "2.8.8",
|
||||||
"prettier": "^2.3.1",
|
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"semver": "^7.5.0",
|
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.38.2",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
},
|
},
|
||||||
|
@ -48,9 +46,9 @@
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
"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:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && 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: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",
|
"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",
|
"test": "lerna run --stream test --stream",
|
||||||
|
@ -67,6 +65,7 @@
|
||||||
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"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: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": "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: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: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 .",
|
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||||
|
@ -95,19 +94,7 @@
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/*"
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -31,4 +31,6 @@ const config: Config.InitialOptions = {
|
||||||
coverageReporters: ["lcov", "json", "clover"],
|
coverageReporters: ["lcov", "json", "clover"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.env.DISABLE_PINO_LOGGER = "1"
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bull": "4.10.1",
|
"bull": "4.10.1",
|
||||||
"correlation-id": "4.0.0",
|
"correlation-id": "4.0.0",
|
||||||
|
|
|
@ -57,6 +57,9 @@ class Replication {
|
||||||
appReplicateOpts() {
|
appReplicateOpts() {
|
||||||
return {
|
return {
|
||||||
filter: (doc: any) => {
|
filter: (doc: any) => {
|
||||||
|
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return doc._id !== DocumentType.APP_METADATA
|
return doc._id !== DocumentType.APP_METADATA
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -9,3 +9,4 @@ export * from "../constants/db"
|
||||||
export { getGlobalDBName, baseGlobalDBName } from "../context"
|
export { getGlobalDBName, baseGlobalDBName } from "../context"
|
||||||
export * from "./lucene"
|
export * from "./lucene"
|
||||||
export * as searchIndexes from "./searchIndexes"
|
export * as searchIndexes from "./searchIndexes"
|
||||||
|
export * from "./errors"
|
||||||
|
|
|
@ -81,8 +81,19 @@ export function generateAppUserID(prodAppId: string, userId: string) {
|
||||||
* Generates a new role ID.
|
* Generates a new role ID.
|
||||||
* @returns {string} The new role ID which the role doc can be stored under.
|
* @returns {string} The new role ID which the role doc can be stored under.
|
||||||
*/
|
*/
|
||||||
export function generateRoleID(id?: any) {
|
export function generateRoleID(name: string) {
|
||||||
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,10 +14,15 @@ async function servedBuilder(timezone: string) {
|
||||||
await publishEvent(Event.SERVED_BUILDER, properties)
|
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 = {
|
const properties: AppServedEvent = {
|
||||||
appVersion: app.version,
|
appVersion: app.version,
|
||||||
timezone,
|
timezone,
|
||||||
|
embed: embed === true,
|
||||||
}
|
}
|
||||||
await publishEvent(Event.SERVED_APP, properties)
|
await publishEvent(Event.SERVED_APP, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,6 @@ export function logAlertWithInfo(
|
||||||
logAlert(message, error)
|
logAlert(message, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logWarn(message: string) {
|
export function logWarn(message: string, e?: any) {
|
||||||
console.warn(`bb-warn: ${message}`)
|
console.warn(`bb-warn: ${message}`, e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import * as google from "../sso/google"
|
import * as google from "../sso/google"
|
||||||
import { Cookie } from "../../../constants"
|
import { Cookie } from "../../../constants"
|
||||||
import { clearCookie, getCookie } from "../../../utils"
|
|
||||||
import { doWithDB } from "../../../db"
|
|
||||||
import * as configs from "../../../configs"
|
import * as configs from "../../../configs"
|
||||||
import { BBContext, Database, 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 { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
|
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
type Passport = {
|
type Passport = {
|
||||||
|
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
|
||||||
|
|
||||||
export async function preAuth(
|
export async function preAuth(
|
||||||
passport: Passport,
|
passport: Passport,
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
next: Function
|
next: Function
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
|
@ -36,8 +37,8 @@ export async function preAuth(
|
||||||
ssoSaveUserNoOp
|
ssoSaveUserNoOp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
if (!ctx.query.appId) {
|
||||||
ctx.throw(400, "appId and datasourceId query params not present.")
|
ctx.throw(400, "appId query param not present.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
|
@ -49,7 +50,7 @@ export async function preAuth(
|
||||||
|
|
||||||
export async function postAuth(
|
export async function postAuth(
|
||||||
passport: Passport,
|
passport: Passport,
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
next: Function
|
next: Function
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
|
@ -57,7 +58,7 @@ export async function postAuth(
|
||||||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
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(
|
return passport.authenticate(
|
||||||
new GoogleStrategy(
|
new GoogleStrategy(
|
||||||
|
@ -69,33 +70,26 @@ export async function postAuth(
|
||||||
(
|
(
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile: SSOProfile,
|
_profile: SSOProfile,
|
||||||
done: Function
|
done: Function
|
||||||
) => {
|
) => {
|
||||||
clearCookie(ctx, Cookie.DatasourceAuth)
|
utils.clearCookie(ctx, Cookie.DatasourceAuth)
|
||||||
done(null, { accessToken, refreshToken })
|
done(null, { accessToken, refreshToken })
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err: any, tokens: string[]) => {
|
async (err: any, tokens: string[]) => {
|
||||||
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
||||||
// update the DB for the datasource with all the user info
|
|
||||||
await doWithDB(authStateCookie.appId, async (db: Database) => {
|
const id = utils.newid()
|
||||||
let datasource
|
await cache.store(
|
||||||
try {
|
`datasource:creation:${authStateCookie.appId}:google:${id}`,
|
||||||
datasource = await db.get(authStateCookie.datasourceId)
|
{
|
||||||
} catch (err: any) {
|
tokens,
|
||||||
if (err.status === 404) {
|
|
||||||
ctx.redirect(baseUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!datasource.config) {
|
)
|
||||||
datasource.config = {}
|
|
||||||
}
|
ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`)
|
||||||
datasource.config.auth = { type: "google", ...tokens }
|
|
||||||
await db.put(datasource)
|
|
||||||
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
|
import fs from "fs"
|
||||||
|
import zlib from "zlib"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { join } from "path"
|
||||||
|
|
||||||
const ALGO = "aes-256-ctr"
|
const ALGO = "aes-256-ctr"
|
||||||
const SEPARATOR = "-"
|
const SEPARATOR = "-"
|
||||||
const ITERATIONS = 10000
|
const ITERATIONS = 10000
|
||||||
const RANDOM_BYTES = 16
|
|
||||||
const STRETCH_LENGTH = 32
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
|
const SALT_LENGTH = 16
|
||||||
|
const IV_LENGTH = 16
|
||||||
|
|
||||||
export enum SecretOption {
|
export enum SecretOption {
|
||||||
API = "api",
|
API = "api",
|
||||||
ENCRYPTION = "encryption",
|
ENCRYPTION = "encryption",
|
||||||
|
@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string {
|
||||||
return secret
|
return secret
|
||||||
}
|
}
|
||||||
|
|
||||||
function stretchString(string: string, salt: Buffer) {
|
function stretchString(secret: string, salt: Buffer) {
|
||||||
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encrypt(
|
export function encrypt(
|
||||||
input: string,
|
input: string,
|
||||||
secretOption: SecretOption = SecretOption.API
|
secretOption: SecretOption = SecretOption.API
|
||||||
) {
|
) {
|
||||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||||
const stretched = stretchString(getSecret(secretOption), salt)
|
const stretched = stretchString(getSecret(secretOption), salt)
|
||||||
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||||
const base = cipher.update(input)
|
const base = cipher.update(input)
|
||||||
|
@ -60,3 +65,115 @@ export function decrypt(
|
||||||
const final = decipher.final()
|
const final = decipher.final()
|
||||||
return Buffer.concat([base, final]).toString()
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
|
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
|
||||||
import { generateRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
|
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
|
||||||
import { getAppDB } from "../context"
|
import { getAppDB } from "../context"
|
||||||
import { doWithDB } from "../db"
|
import { doWithDB } from "../db"
|
||||||
import { Screen, Role as RoleDoc } from "@budibase/types"
|
import { Screen, Role as RoleDoc } from "@budibase/types"
|
||||||
|
@ -25,18 +25,28 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [
|
||||||
BUILTIN_IDS.PUBLIC,
|
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 {
|
export class Role implements RoleDoc {
|
||||||
_id: string
|
_id: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
name: string
|
name: string
|
||||||
permissionId: string
|
permissionId: string
|
||||||
inherits?: string
|
inherits?: string
|
||||||
|
version?: string
|
||||||
permissions = {}
|
permissions = {}
|
||||||
|
|
||||||
constructor(id: string, name: string, permissionId: string) {
|
constructor(id: string, name: string, permissionId: string) {
|
||||||
this._id = id
|
this._id = id
|
||||||
this.name = name
|
this.name = name
|
||||||
this.permissionId = permissionId
|
this.permissionId = permissionId
|
||||||
|
// version for managing the ID - removing the role_ when responding
|
||||||
|
this.version = RoleIDVersion.NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
addInheritance(inherits: string) {
|
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
|
* 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.
|
* to check if the role inherits any others.
|
||||||
* @param {string|null} roleId The level ID to lookup.
|
* @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.
|
* @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) {
|
if (!roleId) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -153,14 +167,20 @@ export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
||||||
role = cloneDeep(
|
role = cloneDeep(
|
||||||
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
|
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 {
|
try {
|
||||||
const db = getAppDB()
|
const db = getAppDB()
|
||||||
const dbRole = await db.get(getDBRoleID(roleId))
|
const dbRole = await db.get(getDBRoleID(roleId))
|
||||||
role = Object.assign(role, dbRole)
|
role = Object.assign(role, dbRole)
|
||||||
// finalise the ID
|
// finalise the ID
|
||||||
role._id = getExternalRoleID(role._id)
|
role._id = getExternalRoleID(role._id, role.version)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!isBuiltin(roleId) && opts?.defaultPublic) {
|
||||||
|
return cloneDeep(BUILTIN_ROLES.PUBLIC)
|
||||||
|
}
|
||||||
// only throw an error if there is no role at all
|
// only throw an error if there is no role at all
|
||||||
if (Object.keys(role).length === 0) {
|
if (Object.keys(role).length === 0) {
|
||||||
throw err
|
throw err
|
||||||
|
@ -254,6 +274,9 @@ export async function getAllRoles(appId?: string) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
roles = body.rows.map((row: any) => row.doc)
|
roles = body.rows.map((row: any) => row.doc)
|
||||||
|
roles.forEach(
|
||||||
|
role => (role._id = getExternalRoleID(role._id!, role.version))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const builtinRoles = getBuiltinRoles()
|
const builtinRoles = getBuiltinRoles()
|
||||||
|
|
||||||
|
@ -261,14 +284,15 @@ export async function getAllRoles(appId?: string) {
|
||||||
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||||
const builtinRole = builtinRoles[builtinRoleId]
|
const builtinRole = builtinRoles[builtinRoleId]
|
||||||
const dbBuiltin = roles.filter(
|
const dbBuiltin = roles.filter(
|
||||||
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
|
dbRole =>
|
||||||
|
getExternalRoleID(dbRole._id!, dbRole.version) === builtinRoleId
|
||||||
)[0]
|
)[0]
|
||||||
if (dbBuiltin == null) {
|
if (dbBuiltin == null) {
|
||||||
roles.push(builtinRole || builtinRoles.BASIC)
|
roles.push(builtinRole || builtinRoles.BASIC)
|
||||||
} else {
|
} else {
|
||||||
// remove role and all back after combining with the builtin
|
// remove role and all back after combining with the builtin
|
||||||
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
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))
|
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).
|
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
||||||
*/
|
*/
|
||||||
export function getDBRoleID(roleId?: string) {
|
export function getDBRoleID(roleName: string) {
|
||||||
if (roleId?.startsWith(DocumentType.ROLE)) {
|
if (roleName?.startsWith(DocumentType.ROLE)) {
|
||||||
return roleId
|
return roleName
|
||||||
}
|
}
|
||||||
return generateRoleID(roleId)
|
return prefixRoleID(roleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
|
* 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_)
|
// 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.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
|
||||||
}
|
}
|
||||||
return roleId
|
return roleId
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let validate = null
|
export let validate = null
|
||||||
|
export let indeterminate = false
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -21,11 +23,19 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
|
<FancyField
|
||||||
|
{error}
|
||||||
|
{value}
|
||||||
|
{validate}
|
||||||
|
{disabled}
|
||||||
|
{compact}
|
||||||
|
clickable
|
||||||
|
on:click={onChange}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<Checkbox {disabled} {value} />
|
<Checkbox {disabled} {value} {indeterminate} />
|
||||||
</span>
|
</span>
|
||||||
<div class="text">
|
<div class="text" class:compact>
|
||||||
{#if text}
|
{#if text}
|
||||||
{text}
|
{text}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -47,6 +57,10 @@
|
||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
.text.compact {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
.text > :global(*) {
|
.text > :global(*) {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -11,6 +11,7 @@
|
||||||
export let value
|
export let value
|
||||||
export let ref
|
export let ref
|
||||||
export let autoHeight
|
export let autoHeight
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const formContext = getContext("fancy-form")
|
const formContext = getContext("fancy-form")
|
||||||
const id = Math.random()
|
const id = Math.random()
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
class:disabled
|
class:disabled
|
||||||
class:focused
|
class:focused
|
||||||
class:clickable
|
class:clickable
|
||||||
|
class:compact
|
||||||
class:auto-height={autoHeight}
|
class:auto-height={autoHeight}
|
||||||
>
|
>
|
||||||
<div class="content" on:click>
|
<div class="content" on:click>
|
||||||
|
@ -61,7 +63,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fancy-field {
|
.fancy-field {
|
||||||
max-width: 400px;
|
|
||||||
background: var(--spectrum-global-color-gray-75);
|
background: var(--spectrum-global-color-gray-75);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -69,6 +70,12 @@
|
||||||
transition: border-color 130ms ease-out, background 130ms ease-out,
|
transition: border-color 130ms ease-out, background 130ms ease-out,
|
||||||
background 130ms ease-out;
|
background 130ms ease-out;
|
||||||
color: var(--spectrum-global-color-gray-800);
|
color: var(--spectrum-global-color-gray-800);
|
||||||
|
--padding: 16px;
|
||||||
|
--height: 64px;
|
||||||
|
}
|
||||||
|
.fancy-field.compact {
|
||||||
|
--padding: 8px;
|
||||||
|
--height: 36px;
|
||||||
}
|
}
|
||||||
.fancy-field:hover {
|
.fancy-field:hover {
|
||||||
border-color: var(--spectrum-global-color-gray-400);
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
@ -91,8 +98,8 @@
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 64px;
|
height: var(--height);
|
||||||
padding: 0 16px;
|
padding: 0 var(--padding);
|
||||||
}
|
}
|
||||||
.fancy-field.auto-height .content {
|
.fancy-field.auto-height .content {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -103,7 +110,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: var(--padding);
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -4,4 +4,5 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
||||||
export { default as FancyButton } from "./FancyButton.svelte"
|
export { default as FancyButton } from "./FancyButton.svelte"
|
||||||
export { default as FancyForm } from "./FancyForm.svelte"
|
export { default as FancyForm } from "./FancyForm.svelte"
|
||||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||||
|
export { default as FancyCheckboxGroup } from "./FancyCheckboxGroup.svelte"
|
||||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let text = null
|
export let text = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size
|
export let size
|
||||||
|
export let indeterminate = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = event => {
|
const onChange = event => {
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
class:checked={value}
|
class:checked={value}
|
||||||
|
class:is-indeterminate={indeterminate}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={value}
|
checked={value}
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
timeOnly,
|
timeOnly,
|
||||||
enableTime,
|
enableTime,
|
||||||
time24hr,
|
time24hr,
|
||||||
|
disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
export let fixed = false
|
export let fixed = false
|
||||||
export let inline = false
|
export let inline = false
|
||||||
|
export let disableCancel = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = fixed || inline
|
let visible = fixed || inline
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancel() {
|
export function cancel() {
|
||||||
if (!visible) {
|
if (!visible || disableCancel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatch("cancel")
|
dispatch("cancel")
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"dev:builder": "routify -c dev:vite",
|
"dev:builder": "routify -c dev:vite",
|
||||||
"dev:vite": "vite --host 0.0.0.0",
|
"dev:vite": "vite --host 0.0.0.0",
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"test": "vitest run"
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -61,6 +61,9 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
showNotificationAction: false,
|
showNotificationAction: false,
|
||||||
sidePanel: false,
|
sidePanel: false,
|
||||||
},
|
},
|
||||||
|
features: {
|
||||||
|
componentValidation: false,
|
||||||
|
},
|
||||||
errors: [],
|
errors: [],
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
libraries: null,
|
libraries: null,
|
||||||
|
@ -74,6 +77,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
propertyFocus: null,
|
propertyFocus: null,
|
||||||
builderSidePanel: false,
|
builderSidePanel: false,
|
||||||
hasLock: true,
|
hasLock: true,
|
||||||
|
showPreview: false,
|
||||||
|
|
||||||
// URL params
|
// URL params
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
|
@ -116,10 +120,13 @@ export const getFrontendStore = () => {
|
||||||
reset: () => {
|
reset: () => {
|
||||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||||
websocket?.disconnect()
|
websocket?.disconnect()
|
||||||
|
websocket = null
|
||||||
},
|
},
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath, hasLock } = 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)
|
await store.actions.components.refreshDefinitions(application.appId)
|
||||||
|
|
||||||
// Reset store state
|
// Reset store state
|
||||||
|
@ -144,6 +151,11 @@ export const getFrontendStore = () => {
|
||||||
navigation: application.navigation || {},
|
navigation: application.navigation || {},
|
||||||
usedPlugins: application.usedPlugins || [],
|
usedPlugins: application.usedPlugins || [],
|
||||||
hasLock,
|
hasLock,
|
||||||
|
features: {
|
||||||
|
...INITIAL_FRONTEND_STATE.features,
|
||||||
|
...application.features,
|
||||||
|
},
|
||||||
|
icon: application.icon || {},
|
||||||
initialised: true,
|
initialised: true,
|
||||||
}))
|
}))
|
||||||
screenHistoryStore.reset()
|
screenHistoryStore.reset()
|
||||||
|
@ -224,6 +236,7 @@ export const getFrontendStore = () => {
|
||||||
legalDirectChildren = []
|
legalDirectChildren = []
|
||||||
) => {
|
) => {
|
||||||
const type = component._component
|
const type = component._component
|
||||||
|
|
||||||
if (illegalChildren.includes(type)) {
|
if (illegalChildren.includes(type)) {
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
@ -237,10 +250,13 @@ export const getFrontendStore = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "@budibase/standard-components/sidepanel") {
|
||||||
|
illegalChildren = []
|
||||||
|
}
|
||||||
|
|
||||||
const definition = store.actions.components.getDefinition(
|
const definition = store.actions.components.getDefinition(
|
||||||
component._component
|
component._component
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset whitelist for direct children
|
// Reset whitelist for direct children
|
||||||
legalDirectChildren = []
|
legalDirectChildren = []
|
||||||
if (definition?.legalDirectChildren?.length) {
|
if (definition?.legalDirectChildren?.length) {
|
||||||
|
@ -279,9 +295,12 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
// Validate screen structure
|
const state = get(store)
|
||||||
// Temporarily disabled to accommodate migration issues
|
|
||||||
// store.actions.screens.validate(screen)
|
// 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
|
// Check screen definition for any component settings which need updated
|
||||||
store.actions.screens.enrichEmptySettings(screen)
|
store.actions.screens.enrichEmptySettings(screen)
|
||||||
|
@ -292,7 +311,6 @@ export const getFrontendStore = () => {
|
||||||
const routesResponse = await API.fetchAppRoutes()
|
const routesResponse = await API.fetchAppRoutes()
|
||||||
|
|
||||||
// If plugins changed we need to fetch the latest app metadata
|
// If plugins changed we need to fetch the latest app metadata
|
||||||
const state = get(store)
|
|
||||||
let usedPlugins = state.usedPlugins
|
let usedPlugins = state.usedPlugins
|
||||||
if (savedScreen.pluginAdded) {
|
if (savedScreen.pluginAdded) {
|
||||||
const { application } = await API.fetchAppPackage(state.appId)
|
const { application } = await API.fetchAppPackage(state.appId)
|
||||||
|
|
|
@ -78,9 +78,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeLooping() {
|
async function removeLooping() {
|
||||||
let loopBlock = $selectedAutomation?.definition.steps.find(
|
|
||||||
x => x.blockToLoop === block.id
|
|
||||||
)
|
|
||||||
try {
|
try {
|
||||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -89,10 +86,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
let loopBlock = $selectedAutomation?.definition.steps.find(
|
|
||||||
x => x.blockToLoop === block.id
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (loopBlock) {
|
if (loopBlock) {
|
||||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
|
@ -168,8 +161,8 @@
|
||||||
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
.properties
|
.properties
|
||||||
)}
|
)}
|
||||||
block={loopBlock}
|
|
||||||
{webhookModal}
|
{webhookModal}
|
||||||
|
block={loopBlock}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -191,7 +184,7 @@
|
||||||
{#if !isTrigger}
|
{#if !isTrigger}
|
||||||
<div>
|
<div>
|
||||||
<div class="block-options">
|
<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">
|
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||||
Add Looping
|
Add Looping
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -32,7 +32,12 @@
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon s hoverable name="MoreSmallList" />
|
<Icon s hoverable name="MoreSmallList" />
|
||||||
</div>
|
</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="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
Icon,
|
Icon,
|
||||||
|
Checkbox,
|
||||||
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
@ -306,6 +308,11 @@
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canShowField(key, value) {
|
||||||
|
const dependsOn = value?.dependsOn
|
||||||
|
return !dependsOn || !!inputData[dependsOn]
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -317,210 +324,233 @@
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each deprecatedSchemaProperties as [key, value]}
|
{#each deprecatedSchemaProperties as [key, value]}
|
||||||
<div class="block-field">
|
{#if canShowField(key, value)}
|
||||||
{#if key !== "fields"}
|
<div class="block-field">
|
||||||
<Label
|
{#if key !== "fields" && value.type !== "boolean"}
|
||||||
tooltip={value.title === "Binding / Value"
|
<Label
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
tooltip={value.title === "Binding / Value"
|
||||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
? "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}
|
{/if}
|
||||||
{:else if value.customType === "query"}
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||||
<QuerySelector
|
<Select
|
||||||
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)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
value={inputData[key]}
|
||||||
updateOnChange={false}
|
placeholder={false}
|
||||||
|
options={value.enum}
|
||||||
|
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if value.type === "json"}
|
||||||
<div class="test">
|
<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
|
<DrawerBindableInput
|
||||||
fillWidth={true}
|
fillWidth
|
||||||
title={value.title}
|
title={value.title}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type="email"
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
allowJS={false}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
|
||||||
drawerLeft="260px"
|
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}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
|
|
|
@ -44,13 +44,15 @@
|
||||||
<Grid
|
<Grid
|
||||||
{API}
|
{API}
|
||||||
tableId={id}
|
tableId={id}
|
||||||
tableType={$tables.selected?.type}
|
|
||||||
allowAddRows={!isUsersTable}
|
allowAddRows={!isUsersTable}
|
||||||
allowDeleteRows={!isUsersTable}
|
allowDeleteRows={!isUsersTable}
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatetable={handleGridTableUpdate}
|
on:updatetable={handleGridTableUpdate}
|
||||||
>
|
>
|
||||||
|
<svelte:fragment slot="filter">
|
||||||
|
<GridFilterButton />
|
||||||
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="controls">
|
<svelte:fragment slot="controls">
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
<GridCreateViewButton />
|
<GridCreateViewButton />
|
||||||
|
@ -65,7 +67,6 @@
|
||||||
<GridImportButton />
|
<GridImportButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridExportButton />
|
<GridExportButton />
|
||||||
<GridFilterButton />
|
|
||||||
<GridAddColumnModal />
|
<GridAddColumnModal />
|
||||||
<GridEditColumnModal />
|
<GridEditColumnModal />
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
import { ActionButton, notifications } from "@budibase/bbui"
|
||||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||||
import { datasources } from "../../../../stores/backend"
|
import { datasources } from "../../../../stores/backend"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
@ -8,9 +8,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: datasource = findDatasource(table?._id)
|
$: datasource = findDatasource(table?._id)
|
||||||
$: plusTables = datasource?.plus
|
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
|
||||||
? Object.values(datasource?.entities || {})
|
|
||||||
: []
|
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -24,31 +22,32 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRelationship() {
|
const afterSave = ({ action }) => {
|
||||||
try {
|
notifications.success(`Relationship ${action} successfully`)
|
||||||
// Create datasource
|
dispatch("updatecolumns")
|
||||||
await datasources.save(datasource)
|
}
|
||||||
notifications.success(`Relationship information saved.`)
|
|
||||||
dispatch("updatecolumns")
|
const onError = err => {
|
||||||
} catch (err) {
|
notifications.error(`Error saving relationship info: ${err}`)
|
||||||
notifications.error(`Error saving relationship info: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if datasource}
|
{#if datasource}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
icon="DataCorrelated"
|
||||||
|
primary
|
||||||
|
quiet
|
||||||
|
on:click={() => modal.show({ fromTable: table })}
|
||||||
|
>
|
||||||
Define relationship
|
Define relationship
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<CreateEditRelationshipModal
|
||||||
<CreateEditRelationship
|
bind:this={modal}
|
||||||
{datasource}
|
{datasource}
|
||||||
save={saveRelationship}
|
{tables}
|
||||||
close={modal.hide}
|
{afterSave}
|
||||||
{plusTables}
|
{onError}
|
||||||
selectedFromTable={table}
|
/>
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
$: tempValue = filters || []
|
$: tempValue = filters || []
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
$: text = getText(filters)
|
||||||
|
|
||||||
|
const getText = filters => {
|
||||||
|
const count = filters?.length
|
||||||
|
return count ? `Filter (${count})` : "Filter"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -23,7 +29,7 @@
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
selected={tempValue?.length > 0}
|
selected={tempValue?.length > 0}
|
||||||
>
|
>
|
||||||
Filter
|
{text}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
|
|
||||||
const { columns, tableId, filter, table } = getContext("grid")
|
const { columns, tableId, filter, table } = getContext("grid")
|
||||||
|
|
||||||
|
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||||
|
$: $tableId, filter.set([])
|
||||||
|
|
||||||
const onFilter = e => {
|
const onFilter = e => {
|
||||||
filter.set(e.detail || [])
|
filter.set(e.detail || [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
|
||||||
const { rows, tableId, tableType } = getContext("grid")
|
const { rows, tableId, table } = getContext("grid")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ImportButton
|
<ImportButton
|
||||||
{disabled}
|
{disabled}
|
||||||
tableId={$tableId}
|
tableId={$tableId}
|
||||||
{tableType}
|
tableType={$table?.type}
|
||||||
on:importrows={rows.actions.refreshData}
|
on:importrows={rows.actions.refreshData}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -182,6 +182,15 @@
|
||||||
indexes,
|
indexes,
|
||||||
})
|
})
|
||||||
dispatch("updatecolumns")
|
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) {
|
if (originalName) {
|
||||||
notifications.success("Column updated successfully")
|
notifications.success("Column updated successfully")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -12,15 +12,14 @@
|
||||||
let selectedRole = BASE_ROLE
|
let selectedRole = BASE_ROLE
|
||||||
let errors = []
|
let errors = []
|
||||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||||
|
let validRegex = /^[a-zA-Z0-9_]*$/
|
||||||
// Don't allow editing of public role
|
// Don't allow editing of public role
|
||||||
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
|
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
|
||||||
$: selectedRoleId = selectedRole._id
|
$: selectedRoleId = selectedRole._id
|
||||||
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
||||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||||
|
|
||||||
$: hasUniqueRoleName = !otherRoles
|
$: roleNameError = getRoleNameError(selectedRole.name)
|
||||||
?.map(role => role.name)
|
|
||||||
?.includes(selectedRole.name)
|
|
||||||
|
|
||||||
$: valid =
|
$: valid =
|
||||||
selectedRole.name &&
|
selectedRole.name &&
|
||||||
|
@ -85,7 +84,7 @@
|
||||||
await roles.save(selectedRole)
|
await roles.save(selectedRole)
|
||||||
notifications.success("Role saved successfully")
|
notifications.success("Role saved successfully")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving role")
|
notifications.error(`Error saving role - ${error.message}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +96,20 @@
|
||||||
changeRole()
|
changeRole()
|
||||||
notifications.success("Role deleted successfully")
|
notifications.success("Role deleted successfully")
|
||||||
} catch (error) {
|
} 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"
|
title="Edit Roles"
|
||||||
confirmText={isCreating ? "Create" : "Save"}
|
confirmText={isCreating ? "Create" : "Save"}
|
||||||
onConfirm={saveRole}
|
onConfirm={saveRole}
|
||||||
disabled={!valid || !hasUniqueRoleName}
|
disabled={!valid || roleNameError}
|
||||||
>
|
>
|
||||||
{#if errors.length}
|
{#if errors.length}
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
|
@ -128,8 +140,8 @@
|
||||||
<Input
|
<Input
|
||||||
label="Name"
|
label="Name"
|
||||||
bind:value={selectedRole.name}
|
bind:value={selectedRole.name}
|
||||||
disabled={shouldDisableRoleInput}
|
disabled={!!selectedRoleId}
|
||||||
error={!hasUniqueRoleName ? "Select a unique role name." : null}
|
error={roleNameError}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Inherits Role"
|
label="Inherits Role"
|
||||||
|
|
|
@ -92,13 +92,20 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadWithBlob(data, filename) {
|
||||||
|
download(new Blob([data], { type: "text/plain" }), filename)
|
||||||
|
}
|
||||||
|
|
||||||
async function exportView() {
|
async function exportView() {
|
||||||
try {
|
try {
|
||||||
const data = await API.exportView({
|
const data = await API.exportView({
|
||||||
viewName: view,
|
viewName: view,
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
})
|
})
|
||||||
download(data, `export.${exportFormat === "csv" ? "csv" : "json"}`)
|
downloadWithBlob(
|
||||||
|
data,
|
||||||
|
`export.${exportFormat === "csv" ? "csv" : "json"}`
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
||||||
}
|
}
|
||||||
|
@ -111,7 +118,7 @@
|
||||||
rows: selectedRows.map(row => row._id),
|
rows: selectedRows.map(row => row._id),
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
})
|
})
|
||||||
download(data, `export.${exportFormat}`)
|
downloadWithBlob(data, `export.${exportFormat}`)
|
||||||
} else if (filters || sorting) {
|
} else if (filters || sorting) {
|
||||||
let response
|
let response
|
||||||
try {
|
try {
|
||||||
|
@ -130,7 +137,7 @@
|
||||||
notifications.error("Export Failed")
|
notifications.error("Export Failed")
|
||||||
}
|
}
|
||||||
if (response) {
|
if (response) {
|
||||||
download(response, `export.${exportFormat}`)
|
downloadWithBlob(response, `export.${exportFormat}`)
|
||||||
notifications.success("Export Successful")
|
notifications.success("Export Successful")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
export let tableType
|
export let tableType
|
||||||
|
|
||||||
let rows = []
|
let rows = []
|
||||||
let allValid = false
|
let allValid = false
|
||||||
let displayColumn = null
|
let displayColumn = null
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
customQueryText,
|
customQueryText,
|
||||||
} from "helpers/data/utils"
|
} from "helpers/data/utils"
|
||||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||||
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
|
|
||||||
$: enrichedDataSources = enrichDatasources(
|
$: enrichedDataSources = enrichDatasources(
|
||||||
$datasources,
|
$datasources,
|
||||||
$params,
|
$params,
|
||||||
|
@ -71,6 +73,13 @@
|
||||||
$goto(`./datasource/${datasource._id}`)
|
$goto(`./datasource/${datasource._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectTable = tableId => {
|
||||||
|
tables.select(tableId)
|
||||||
|
if (!$isActive("./table/:tableId")) {
|
||||||
|
$goto(`./table/${tableId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeNode(datasource) {
|
function closeNode(datasource) {
|
||||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
||||||
}
|
}
|
||||||
|
@ -151,9 +160,16 @@
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
<div class="hierarchy-items-container">
|
<div class="hierarchy-items-container">
|
||||||
|
<NavItem
|
||||||
|
icon="UserGroup"
|
||||||
|
text="Users"
|
||||||
|
selected={$isActive("./table/:tableId") &&
|
||||||
|
$tables.selected?._id === TableNames.USERS}
|
||||||
|
on:click={() => selectTable(TableNames.USERS)}
|
||||||
|
/>
|
||||||
{#each enrichedDataSources as datasource, idx}
|
{#each enrichedDataSources as datasource, idx}
|
||||||
<NavItem
|
<NavItem
|
||||||
border={idx > 0}
|
border
|
||||||
text={datasource.name}
|
text={datasource.name}
|
||||||
opened={datasource.open}
|
opened={datasource.open}
|
||||||
selected={$isActive("./datasource") && datasource.selected}
|
selected={$isActive("./datasource") && datasource.selected}
|
||||||
|
@ -174,7 +190,7 @@
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if datasource.open}
|
{#if datasource.open}
|
||||||
<TableNavigator sourceId={datasource._id} />
|
<TableNavigator sourceId={datasource._id} {selectTable} />
|
||||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
||||||
<NavItem
|
<NavItem
|
||||||
indentLevel={1}
|
indentLevel={1}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -44,6 +44,9 @@ export default ICONS
|
||||||
|
|
||||||
export function getIcon(integrationType, schema) {
|
export function getIcon(integrationType, schema) {
|
||||||
const integrationList = get(integrations)
|
const integrationList = get(integrations)
|
||||||
|
if (!integrationList) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (integrationList[integrationType]?.iconUrl) {
|
if (integrationList[integrationType]?.iconUrl) {
|
||||||
return { url: integrationList[integrationType].iconUrl }
|
return { url: integrationList[integrationType].iconUrl }
|
||||||
} else if (schema?.custom || !ICONS[integrationType]) {
|
} else if (schema?.custom || !ICONS[integrationType]) {
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,43 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
|
||||||
import { IntegrationNames } from "constants/backend"
|
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
|
||||||
import { saveDatasource as save } from "builderStore/datasource"
|
|
||||||
import { organisation } from "stores/portal"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let integration
|
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
|
||||||
let datasource = cloneDeep(integration)
|
|
||||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await organisation.init()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
|
||||||
cancelText="Back"
|
|
||||||
size="L"
|
|
||||||
>
|
|
||||||
<!-- 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 {IntegrationNames[
|
|
||||||
datasource.type
|
|
||||||
]} integration.</Body
|
|
||||||
>
|
|
||||||
</Layout>
|
|
||||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
|
||||||
{:else if isGoogleConfigured === false}
|
|
||||||
<Body size="S"
|
|
||||||
>Google authentication is not enabled, please complete Google SSO
|
|
||||||
configuration.</Body
|
|
||||||
>
|
|
||||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
|
||||||
{/if}
|
|
||||||
</ModalContent>
|
|
|
@ -1,7 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { datasources } from "stores/backend"
|
import { get } from "svelte/store"
|
||||||
|
import { datasources, integrations } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { Input, ModalContent, Modal } from "@budibase/bbui"
|
import { Input, ModalContent, Modal } from "@budibase/bbui"
|
||||||
|
import { integrationForDatasource } from "stores/selectors"
|
||||||
|
|
||||||
let error = ""
|
let error = ""
|
||||||
let modal
|
let modal
|
||||||
|
@ -32,7 +34,10 @@
|
||||||
...datasource,
|
...datasource,
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
await datasources.save(updatedDatasource)
|
await datasources.update({
|
||||||
|
datasource: updatedDatasource,
|
||||||
|
integration: integrationForDatasource(get(integrations), datasource),
|
||||||
|
})
|
||||||
notifications.success(`Datasource ${name} updated successfully.`)
|
notifications.success(`Datasource ${name} updated successfully.`)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
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 false
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
FancyCheckboxGroup,
|
||||||
|
InlineAlert,
|
||||||
|
Layout,
|
||||||
|
ModalContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { createTableSelectionStore } from "./tableSelectionStore"
|
||||||
|
|
||||||
|
export let integration
|
||||||
|
export let datasource
|
||||||
|
export let onComplete = () => {}
|
||||||
|
|
||||||
|
$: store = createTableSelectionStore(integration, datasource)
|
||||||
|
|
||||||
|
$: isSheets = integration.name === IntegrationTypes.GOOGLE_SHEETS
|
||||||
|
$: tableType = isSheets ? "sheets" : "tables"
|
||||||
|
$: title = `Choose your ${tableType}`
|
||||||
|
|
||||||
|
$: confirmText =
|
||||||
|
$store.loading || $store.hasSelected
|
||||||
|
? `Fetch ${tableType}`
|
||||||
|
: "Continue without fetching"
|
||||||
|
|
||||||
|
$: description = isSheets
|
||||||
|
? "Select which spreadsheets you want to connect."
|
||||||
|
: "Choose what tables you want to sync with Budibase"
|
||||||
|
|
||||||
|
$: selectAllText = isSheets ? "Select all sheets" : "Select all"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
{title}
|
||||||
|
cancelText="Skip"
|
||||||
|
size="L"
|
||||||
|
{confirmText}
|
||||||
|
onConfirm={() => store.importSelectedTables(onComplete)}
|
||||||
|
disabled={$store.loading}
|
||||||
|
>
|
||||||
|
{#if $store.loading}
|
||||||
|
<div class="loading">
|
||||||
|
<Spinner size="20" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Layout noPadding no>
|
||||||
|
<Body size="S">{description}</Body>
|
||||||
|
|
||||||
|
<FancyCheckboxGroup
|
||||||
|
options={$store.tableNames}
|
||||||
|
selected={$store.selectedTableNames}
|
||||||
|
on:change={e => store.setSelectedTableNames(e.detail)}
|
||||||
|
{selectAllText}
|
||||||
|
/>
|
||||||
|
{#if $store.error}
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header={$store.error.title}
|
||||||
|
message={$store.error.description}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { derived, writable, get } from "svelte/store"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { datasources, ImportTableError } from "stores/backend"
|
||||||
|
|
||||||
|
export const createTableSelectionStore = (integration, datasource) => {
|
||||||
|
const tableNamesStore = writable([])
|
||||||
|
const selectedTableNamesStore = writable([])
|
||||||
|
const errorStore = writable(null)
|
||||||
|
const loadingStore = writable(true)
|
||||||
|
|
||||||
|
datasources.getTableNames(datasource).then(tableNames => {
|
||||||
|
tableNamesStore.set(tableNames)
|
||||||
|
|
||||||
|
selectedTableNamesStore.set(
|
||||||
|
tableNames.filter(tableName => datasource.entities[tableName])
|
||||||
|
)
|
||||||
|
|
||||||
|
loadingStore.set(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const setSelectedTableNames = selectedTableNames => {
|
||||||
|
selectedTableNamesStore.set(selectedTableNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
const importSelectedTables = async onComplete => {
|
||||||
|
errorStore.set(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await datasources.updateSchema(datasource, get(selectedTableNamesStore))
|
||||||
|
|
||||||
|
notifications.success(`Tables fetched successfully.`)
|
||||||
|
await onComplete()
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ImportTableError) {
|
||||||
|
errorStore.set(err)
|
||||||
|
} else {
|
||||||
|
notifications.error("Error fetching tables.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent modal closing
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = derived(
|
||||||
|
[tableNamesStore, selectedTableNamesStore, errorStore, loadingStore],
|
||||||
|
([
|
||||||
|
$tableNamesStore,
|
||||||
|
$selectedTableNamesStore,
|
||||||
|
$errorStore,
|
||||||
|
$loadingStore,
|
||||||
|
]) => {
|
||||||
|
return {
|
||||||
|
tableNames: $tableNamesStore,
|
||||||
|
selectedTableNames: $selectedTableNamesStore,
|
||||||
|
error: $errorStore,
|
||||||
|
loading: $loadingStore,
|
||||||
|
hasSelected: $selectedTableNamesStore.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: combined.subscribe,
|
||||||
|
setSelectedTableNames,
|
||||||
|
importSelectedTables,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Menu, Icon, MenuSection, MenuItem } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let heading
|
|
||||||
export let tables
|
|
||||||
export let selected = false
|
|
||||||
export let select
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
<MenuSection {heading}>
|
|
||||||
{#each tables as table}
|
|
||||||
<MenuItem noClose icon="Table" on:click={() => select(table)}>
|
|
||||||
{table.name}
|
|
||||||
{#if selected}
|
|
||||||
<Icon size="S" name="Checkmark" />
|
|
||||||
{/if}
|
|
||||||
</MenuItem>
|
|
||||||
{/each}
|
|
||||||
</MenuSection>
|
|
||||||
</Menu>
|
|
|
@ -10,17 +10,13 @@
|
||||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
|
||||||
export let sourceId
|
export let sourceId
|
||||||
|
export let selectTable
|
||||||
|
|
||||||
$: sortedTables = $tables.list
|
$: sortedTables = $tables.list
|
||||||
.filter(table => table.sourceId === sourceId)
|
.filter(
|
||||||
|
table => table.sourceId === sourceId && table._id !== TableNames.USERS
|
||||||
|
)
|
||||||
.sort(alphabetical)
|
.sort(alphabetical)
|
||||||
|
|
||||||
const selectTable = tableId => {
|
|
||||||
tables.select(tableId)
|
|
||||||
if (!$isActive("./table/:tableId")) {
|
|
||||||
$goto(`./table/${tableId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
name: "Automations",
|
name: "Automations",
|
||||||
description: "",
|
description: "",
|
||||||
icon: "Compass",
|
icon: "Compass",
|
||||||
action: () => $goto("./automate"),
|
action: () => $goto("./automation"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "Publish",
|
type: "Publish",
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
name: "App",
|
name: "App",
|
||||||
description: "",
|
description: "",
|
||||||
icon: "Play",
|
icon: "Play",
|
||||||
action: () => window.open(`/${$store.appId}`),
|
action: () => store.update(state => ({ ...state, showPreview: true })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "Preview",
|
type: "Preview",
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
type: "Automation",
|
type: "Automation",
|
||||||
name: automation.name,
|
name: automation.name,
|
||||||
icon: "ShareAndroid",
|
icon: "ShareAndroid",
|
||||||
action: () => $goto(`./automate/${automation._id}`),
|
action: () => $goto(`./automation/${automation._id}`),
|
||||||
})),
|
})),
|
||||||
...Constants.Themes.map(theme => ({
|
...Constants.Themes.map(theme => ({
|
||||||
type: "Change Builder Theme",
|
type: "Change Builder Theme",
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
faColumns,
|
faColumns,
|
||||||
faArrowsAlt,
|
faArrowsAlt,
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
|
faCircleCheck,
|
||||||
|
faGear,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
|
@ -48,8 +50,11 @@
|
||||||
faEye,
|
faEye,
|
||||||
faColumns,
|
faColumns,
|
||||||
faArrowsAlt,
|
faArrowsAlt,
|
||||||
faQuestionCircle
|
faQuestionCircle,
|
||||||
// --
|
// --
|
||||||
|
|
||||||
|
faCircleCheck,
|
||||||
|
faGear
|
||||||
)
|
)
|
||||||
dom.watch()
|
dom.watch()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -469,10 +469,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.overlay-wrap {
|
.overlay-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.mode-overlay {
|
.mode-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
on:blur={() => dispatch("blur")}
|
on:blur={() => dispatch("blur")}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{error}
|
{error}
|
||||||
|
options={allOptions}
|
||||||
/>
|
/>
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="icon" on:click={bindingDrawer.show}>
|
<div class="icon" on:click={bindingDrawer.show}>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let drawerLeft
|
export let drawerLeft
|
||||||
export let key
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
|
|
@ -3,34 +3,45 @@
|
||||||
notifications,
|
notifications,
|
||||||
Popover,
|
Popover,
|
||||||
Layout,
|
Layout,
|
||||||
Heading,
|
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
export let loaded
|
||||||
|
|
||||||
let publishPopover
|
|
||||||
let publishPopoverAnchor
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
|
let updateAppModal
|
||||||
|
let revertModal
|
||||||
|
let versionModal
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(
|
let appActionPopover
|
||||||
app => app.devId === application && app.status === "published"
|
let appActionPopoverOpen = false
|
||||||
)
|
let appActionPopoverAnchor
|
||||||
|
|
||||||
|
let publishing = false
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
|
||||||
$: deployments = []
|
$: deployments = []
|
||||||
|
@ -38,7 +49,29 @@
|
||||||
.filter(deployment => deployment.status === "SUCCESS")
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
$: isPublished = selectedApp && latestDeployments?.length > 0
|
$: isPublished =
|
||||||
|
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||||
|
|
||||||
|
$: updateAvailable =
|
||||||
|
$store.upgradableVersion &&
|
||||||
|
$store.version &&
|
||||||
|
$store.upgradableVersion !== $store.version
|
||||||
|
|
||||||
|
$: canPublish = !publishing && loaded
|
||||||
|
|
||||||
|
const initialiseApp = async () => {
|
||||||
|
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||||
|
await store.actions.initialise(applicationPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDeploymentString = () => {
|
||||||
|
return deployments?.length
|
||||||
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
|
time:
|
||||||
|
new Date().getTime() - new Date(deployments[0].updatedAt).getTime(),
|
||||||
|
})
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||||
if (deployments.length > 0) {
|
if (deployments.length > 0) {
|
||||||
|
@ -62,7 +95,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewApp = () => {
|
const previewApp = () => {
|
||||||
window.open(`/${application}`)
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
showPreview: true,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewApp = () => {
|
const viewApp = () => {
|
||||||
|
@ -77,11 +113,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function publishApp() {
|
||||||
|
try {
|
||||||
|
publishing = true
|
||||||
|
|
||||||
|
await API.publishAppChanges($store.appId)
|
||||||
|
|
||||||
|
notifications.send("App published", {
|
||||||
|
type: "success",
|
||||||
|
icon: "GlobeCheck",
|
||||||
|
})
|
||||||
|
|
||||||
|
await completePublish()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
analytics.captureException(error)
|
||||||
|
notifications.error("Error publishing app")
|
||||||
|
}
|
||||||
|
publishing = false
|
||||||
|
}
|
||||||
|
|
||||||
const unpublishApp = () => {
|
const unpublishApp = () => {
|
||||||
publishPopover.hide()
|
appActionPopover.hide()
|
||||||
unpublishModal.show()
|
unpublishModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const revertApp = () => {
|
||||||
|
appActionPopover.hide()
|
||||||
|
revertModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
const confirmUnpublishApp = async () => {
|
const confirmUnpublishApp = async () => {
|
||||||
if (!application || !isPublished) {
|
if (!application || !isPublished) {
|
||||||
//confirm the app has loaded.
|
//confirm the app has loaded.
|
||||||
|
@ -90,7 +151,10 @@
|
||||||
try {
|
try {
|
||||||
await API.unpublishApp(selectedApp.prodId)
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
await apps.load()
|
await apps.load()
|
||||||
notifications.success("App unpublished successfully")
|
notifications.send("App unpublished", {
|
||||||
|
type: "success",
|
||||||
|
icon: "GlobeStrike",
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error unpublishing app")
|
notifications.error("Error unpublishing app")
|
||||||
}
|
}
|
||||||
|
@ -114,97 +178,161 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $store.hasLock}
|
{#if $store.hasLock}
|
||||||
<div class="action-top-nav">
|
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<div class="version">
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<VersionModal />
|
{#if updateAvailable}
|
||||||
</div>
|
<div class="app-action-button version" on:click={versionModal.show}>
|
||||||
<RevertModal />
|
<div class="app-action">
|
||||||
|
<ActionButton quiet>
|
||||||
{#if isPublished}
|
<StatusLight notice />
|
||||||
<div class="publish-popover">
|
Update
|
||||||
<div bind:this={publishPopoverAnchor}>
|
</ActionButton>
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Globe"
|
|
||||||
size="M"
|
|
||||||
tooltip="Your published app"
|
|
||||||
on:click={publishPopover.show()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
|
||||||
bind:this={publishPopover}
|
|
||||||
align="right"
|
|
||||||
disabled={!isPublished}
|
|
||||||
anchor={publishPopoverAnchor}
|
|
||||||
offset={10}
|
|
||||||
>
|
|
||||||
<div class="popover-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<Heading size="XS">Your published app</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
<span class="publish-popover-message">
|
|
||||||
{processStringSync(
|
|
||||||
"Last published {{ duration time 'millisecond' }} ago",
|
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() -
|
|
||||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button
|
|
||||||
warning={true}
|
|
||||||
icon="GlobeStrike"
|
|
||||||
disabled={!isPublished}
|
|
||||||
on:click={unpublishApp}
|
|
||||||
>
|
|
||||||
Unpublish
|
|
||||||
</Button>
|
|
||||||
<Button cta on:click={viewApp}>View app</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isPublished}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="GlobeStrike"
|
|
||||||
size="M"
|
|
||||||
tooltip="Your app has not been published yet"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<TourWrap
|
<TourWrap
|
||||||
tourStepKey={$store.onboarding
|
tourStepKey={$store.onboarding
|
||||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||||
>
|
>
|
||||||
<span id="builder-app-users-button">
|
<div class="app-action-button users">
|
||||||
<ActionButton
|
<div class="app-action" id="builder-app-users-button">
|
||||||
quiet
|
<ActionButton
|
||||||
icon="UserGroup"
|
quiet
|
||||||
size="M"
|
icon="UserGroup"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.builderSidePanel = true
|
state.builderSidePanel = true
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
</TourWrap>
|
</TourWrap>
|
||||||
|
|
||||||
|
<div class="app-action-button preview">
|
||||||
|
<div class="app-action">
|
||||||
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
|
Preview
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div
|
||||||
|
class="app-action-button publish app-action-popover"
|
||||||
|
on:click={() => {
|
||||||
|
if (!appActionPopoverOpen) {
|
||||||
|
appActionPopover.show()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div bind:this={appActionPopoverAnchor}>
|
||||||
|
<div class="app-action">
|
||||||
|
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||||
|
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||||
|
<span class="publish-open" id="builder-app-publish-button">
|
||||||
|
Publish
|
||||||
|
<Icon
|
||||||
|
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
||||||
|
size="M"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</TourWrap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={appActionPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
anchor={appActionPopoverAnchor}
|
||||||
|
offset={35}
|
||||||
|
on:close={() => {
|
||||||
|
appActionPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
appActionPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="app-action-popover-content">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<Body size="M">
|
||||||
|
<span
|
||||||
|
class="app-link"
|
||||||
|
on:click={() => {
|
||||||
|
if (isPublished) {
|
||||||
|
viewApp()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
updateAppModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$store.url}
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon size="S" name="LinkOut" />
|
||||||
|
{:else}
|
||||||
|
<Icon size="S" name="Edit" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<Body size="S">
|
||||||
|
<span class="publish-popover-status">
|
||||||
|
{#if isPublished}
|
||||||
|
<span class="status-text">
|
||||||
|
{updateDeploymentString(deployments)}
|
||||||
|
</span>
|
||||||
|
<span class="unpublish-link">
|
||||||
|
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||||
|
</span>
|
||||||
|
<span class="revert-link">
|
||||||
|
<Link quiet secondary on:click={revertApp}>Revert</Link>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="status-text unpublished">Not published</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="action-buttons">
|
||||||
|
{#if $store.hasLock}
|
||||||
|
{#if isPublished}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Code"
|
||||||
|
on:click={() => {
|
||||||
|
$goto("./settings/embed")
|
||||||
|
appActionPopover.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Embed
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={publishApp}
|
||||||
|
id={"builder-app-publish-button"}
|
||||||
|
disabled={!canPublish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={unpublishModal}
|
bind:this={unpublishModal}
|
||||||
title="Confirm unpublish"
|
title="Confirm unpublish"
|
||||||
|
@ -213,45 +341,122 @@
|
||||||
>
|
>
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal
|
||||||
|
app={{
|
||||||
|
name: $store.name,
|
||||||
|
url: $store.url,
|
||||||
|
icon: $store.icon,
|
||||||
|
appId: $store.appId,
|
||||||
|
}}
|
||||||
|
onUpdateComplete={async () => {
|
||||||
|
await initialiseApp()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<RevertModal bind:this={revertModal} />
|
||||||
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
{:else}
|
||||||
|
<div class="app-action-button preview-locked">
|
||||||
|
<div class="app-action">
|
||||||
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
|
Preview
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<Button on:click={previewApp} secondary>Preview</Button>
|
|
||||||
{#if $store.hasLock}
|
|
||||||
<DeployModal onOk={completePublish} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* .banner-btn {
|
.app-action-popover-content {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
} */
|
|
||||||
.popover-content {
|
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
width: 360px;
|
||||||
}
|
}
|
||||||
.buttons {
|
|
||||||
display: flex;
|
.app-action-popover-content :global(.icon svg.spectrum-Icon) {
|
||||||
flex-direction: row;
|
height: 0.8em;
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* gap: var(--spacing-s); */
|
height: 100%;
|
||||||
}
|
|
||||||
.version {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-top-nav {
|
.action-top-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.app-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.app-action-popover-content .status-text {
|
||||||
|
color: var(--spectrum-global-color-green-500);
|
||||||
|
border-right: 1px solid var(--spectrum-global-color-gray-500);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover-content .status-text.unpublished {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
border-right: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
.app-action-popover-content .action-buttons {
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover-content
|
||||||
|
.publish-popover-status
|
||||||
|
.unpublish-link
|
||||||
|
:global(.spectrum-Link) {
|
||||||
|
color: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
.publish-popover-status {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover .publish-open {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.publish:hover {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.publish {
|
||||||
|
border-left: var(--border-light);
|
||||||
|
padding: 0px var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.version :global(.spectrum-ActionButton-label) {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spectrum-actionbutton-icon-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.preview-locked {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import { Input, notifications } from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export const show = () => {
|
||||||
|
deletionModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hide = () => {
|
||||||
|
deletionModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
let deletionModal
|
||||||
|
let deletionConfirmationAppName
|
||||||
|
|
||||||
|
const deleteApp = async () => {
|
||||||
|
try {
|
||||||
|
await API.deleteApp($store.appId)
|
||||||
|
apps.load()
|
||||||
|
notifications.success("App deleted successfully")
|
||||||
|
$goto("/builder")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error deleting app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deletionModal}
|
||||||
|
title="Delete app"
|
||||||
|
okText="Delete"
|
||||||
|
onOk={deleteApp}
|
||||||
|
onCancel={() => (deletionConfirmationAppName = null)}
|
||||||
|
disabled={deletionConfirmationAppName !== $store.name}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete <b>{$store.name}</b>?
|
||||||
|
<br />
|
||||||
|
Please enter the app name below to confirm.
|
||||||
|
<br /><br />
|
||||||
|
<Input bind:value={deletionConfirmationAppName} placeholder={$store.name} />
|
||||||
|
</ConfirmDialog>
|
|
@ -1,15 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Input, Modal, notifications, ModalContent } from "@budibase/bbui"
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
ModalContent,
|
|
||||||
ActionButton,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export let disabled = false
|
export let onComplete = () => {}
|
||||||
|
|
||||||
let revertModal
|
let revertModal
|
||||||
let appName
|
let appName
|
||||||
|
@ -24,20 +18,20 @@
|
||||||
const applicationPkg = await API.fetchAppPackage(appId)
|
const applicationPkg = await API.fetchAppPackage(appId)
|
||||||
await store.actions.initialise(applicationPkg)
|
await store.actions.initialise(applicationPkg)
|
||||||
notifications.info("Changes reverted successfully")
|
notifications.info("Changes reverted successfully")
|
||||||
|
onComplete()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error reverting changes: ${error}`)
|
notifications.error(`Error reverting changes: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton
|
export const hide = () => {
|
||||||
quiet
|
revertModal.hide()
|
||||||
icon="Revert"
|
}
|
||||||
size="M"
|
|
||||||
tooltip="Revert changes"
|
export const show = () => {
|
||||||
on:click={revertModal.show}
|
revertModal.show()
|
||||||
{disabled}
|
}
|
||||||
/>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={revertModal}>
|
<Modal bind:this={revertModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
updateModal.hide()
|
updateModal.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let onComplete = () => {}
|
||||||
export let hideIcon = false
|
export let hideIcon = false
|
||||||
|
|
||||||
let updateModal
|
let updateModal
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
notifications.success(
|
notifications.success(
|
||||||
`App updated successfully to version ${$store.upgradableVersion}`
|
`App updated successfully to version ${$store.upgradableVersion}`
|
||||||
)
|
)
|
||||||
|
onComplete()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error updating app: ${err}`)
|
notifications.error(`Error updating app: ${err}`)
|
||||||
}
|
}
|
||||||
|
@ -70,9 +72,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !hideIcon && updateAvailable}
|
{#if !hideIcon && updateAvailable}
|
||||||
<StatusLight hoverable on:click={updateModal.show} notice>
|
<StatusLight hoverable on:click={updateModal.show} notice>Update</StatusLight>
|
||||||
Update available
|
|
||||||
</StatusLight>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
|
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ const componentMap = {
|
||||||
fieldConfiguration: FieldConfiguration,
|
fieldConfiguration: FieldConfiguration,
|
||||||
columns: ColumnEditor,
|
columns: ColumnEditor,
|
||||||
"columns/basic": BasicColumnEditor,
|
"columns/basic": BasicColumnEditor,
|
||||||
|
"columns/grid": GridColumnEditor,
|
||||||
"field/sortable": SortableFieldSelect,
|
"field/sortable": SortableFieldSelect,
|
||||||
"field/string": FormFieldSelect,
|
"field/string": FormFieldSelect,
|
||||||
"field/number": FormFieldSelect,
|
"field/number": FormFieldSelect,
|
||||||
|
|
|
@ -43,7 +43,9 @@
|
||||||
title="Destination"
|
title="Destination"
|
||||||
placeholder="/screen"
|
placeholder="/screen"
|
||||||
value={parameters.url}
|
value={parameters.url}
|
||||||
on:change={value => (parameters.url = value.detail)}
|
on:change={value => {
|
||||||
|
parameters.url = value.detail ? value.detail.trim() : value.detail
|
||||||
|
}}
|
||||||
{bindings}
|
{bindings}
|
||||||
options={urlOptions}
|
options={urlOptions}
|
||||||
appendBindingsAsOptions={false}
|
appendBindingsAsOptions={false}
|
||||||
|
@ -55,7 +57,9 @@
|
||||||
title="Destination"
|
title="Destination"
|
||||||
placeholder="/url"
|
placeholder="/url"
|
||||||
value={parameters.url}
|
value={parameters.url}
|
||||||
on:change={value => (parameters.url = value.detail)}
|
on:change={value => {
|
||||||
|
parameters.url = value.detail ? value.detail.trim() : value.detail
|
||||||
|
}}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
<div />
|
<div />
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { Label, Combobox, Select } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
getActionProviderComponents,
|
||||||
|
buildFormSchema,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "top"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
||||||
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
|
$: actionProviders = getActionProviderComponents(
|
||||||
|
$currentAsset,
|
||||||
|
$store.selectedComponentId,
|
||||||
|
"ScrollTo"
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>Form</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.componentId}
|
||||||
|
options={actionProviders}
|
||||||
|
getOptionLabel={x => x._instanceName}
|
||||||
|
getOptionValue={x => x._id}
|
||||||
|
/>
|
||||||
|
<Label small>Field</Label>
|
||||||
|
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
grid-template-columns: auto;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
|
import { Select, Label, Input, Checkbox, Icon, Body } from "@budibase/bbui"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
|
|
||||||
export let parameters = {}
|
export let parameters = {}
|
||||||
|
@ -11,11 +11,9 @@
|
||||||
NEW: "new",
|
NEW: "new",
|
||||||
EXISTING: "existing",
|
EXISTING: "existing",
|
||||||
}
|
}
|
||||||
|
|
||||||
let automationStatus = parameters.automationId
|
let automationStatus = parameters.automationId
|
||||||
? AUTOMATION_STATUS.EXISTING
|
? AUTOMATION_STATUS.EXISTING
|
||||||
: AUTOMATION_STATUS.NEW
|
: AUTOMATION_STATUS.NEW
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
||||||
parameters.synchronous = false
|
parameters.synchronous = false
|
||||||
|
@ -23,6 +21,7 @@
|
||||||
parameters.synchronous = automations.find(
|
parameters.synchronous = automations.find(
|
||||||
automation => automation._id === parameters.automationId
|
automation => automation._id === parameters.automationId
|
||||||
)?.synchronous
|
)?.synchronous
|
||||||
|
parameters
|
||||||
}
|
}
|
||||||
$: automations = $automationStore.automations
|
$: automations = $automationStore.automations
|
||||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||||
|
@ -42,35 +41,16 @@
|
||||||
synchronous: hasCollectBlock,
|
synchronous: hasCollectBlock,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: hasAutomations = automations && automations.length > 0
|
$: hasAutomations = automations && automations.length > 0
|
||||||
$: selectedAutomation = automations?.find(
|
$: selectedAutomation = automations?.find(
|
||||||
a => a._id === parameters?.automationId
|
a => a._id === parameters?.automationId
|
||||||
)
|
)
|
||||||
$: selectedSchema = selectedAutomation?.schema
|
$: selectedSchema = selectedAutomation?.schema
|
||||||
|
|
||||||
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
||||||
|
|
||||||
const onFieldsChanged = e => {
|
const onFieldsChanged = field => {
|
||||||
parameters.fields = Object.entries(e.detail || {}).reduce(
|
parameters.fields = { ...parameters.fields, ...field }
|
||||||
(acc, [key, value]) => {
|
|
||||||
acc[key.trim()] = value
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setNew = () => {
|
|
||||||
automationStatus = AUTOMATION_STATUS.NEW
|
|
||||||
parameters.automationId = undefined
|
|
||||||
parameters.fields = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setExisting = () => {
|
|
||||||
automationStatus = AUTOMATION_STATUS.EXISTING
|
|
||||||
parameters.newAutomationName = ""
|
|
||||||
parameters.fields = {}
|
|
||||||
parameters.automationId = automations[0]?._id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
|
@ -83,30 +63,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="radios">
|
<div class="fields">
|
||||||
<div class="radio-container" on:click={setNew}>
|
<div class:title-padding={parameters.synchronous}>
|
||||||
<input
|
<Label small>Automation</Label>
|
||||||
type="radio"
|
|
||||||
value={AUTOMATION_STATUS.NEW}
|
|
||||||
bind:group={automationStatus}
|
|
||||||
/>
|
|
||||||
<Label small>Create a new automation</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
|
<div style="width: 100%">
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
value={AUTOMATION_STATUS.EXISTING}
|
|
||||||
bind:group={automationStatus}
|
|
||||||
disabled={!hasAutomations}
|
|
||||||
/>
|
|
||||||
<Label small grey={!hasAutomations}>Use an existing automation</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="params">
|
|
||||||
<Label small>Automation</Label>
|
|
||||||
|
|
||||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
|
||||||
<Select
|
<Select
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:value={parameters.automationId}
|
bind:value={parameters.automationId}
|
||||||
|
@ -115,42 +76,46 @@
|
||||||
getOptionLabel={x => x.name}
|
getOptionLabel={x => x.name}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x._id}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{#if parameters.synchronous}
|
||||||
<Input
|
<div class="synchronous-info">
|
||||||
bind:value={parameters.newAutomationName}
|
<Icon size="XS" name="Info" />
|
||||||
placeholder="Enter automation name"
|
<Body size="XS">This automation will run synchronously</Body>
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if parameters.synchronous}
|
|
||||||
<Label small />
|
|
||||||
|
|
||||||
<div class="synchronous-info">
|
|
||||||
<Icon name="Info" />
|
|
||||||
<div>
|
|
||||||
<i
|
|
||||||
>This automation will run synchronously as it contains a Collect
|
|
||||||
step</i
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
<Label small />
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if parameters.synchronous}
|
||||||
|
<div class="fields">
|
||||||
|
<Label small>Timeout</Label>
|
||||||
<div class="timeout-width">
|
<div class="timeout-width">
|
||||||
<Input
|
<Input type="number" {error} bind:value={parameters.timeout} />
|
||||||
label="Timeout in seconds (120 max)"
|
|
||||||
type="number"
|
|
||||||
{error}
|
|
||||||
bind:value={parameters.timeout}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="fields">
|
||||||
|
{#if selectedSchema && selectedSchema.length}
|
||||||
|
{#each selectedSchema as field, idx}
|
||||||
|
{#if idx === 0}
|
||||||
|
<Label small>Fields</Label>
|
||||||
|
{:else}
|
||||||
|
<Label small />
|
||||||
|
{/if}
|
||||||
|
<Input disabled value={field.name} />
|
||||||
|
<DrawerBindableInput
|
||||||
|
value={parameters.fields && parameters.fields[field.name]}
|
||||||
|
{bindings}
|
||||||
|
on:change={event => onFieldsChanged({ [field.name]: event.detail })}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-margin">
|
||||||
<Label small />
|
<Label small />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
text="Do not display default notification"
|
text="Do not display default notification"
|
||||||
bind:value={parameters.notificationOverride}
|
bind:value={parameters.notificationOverride}
|
||||||
/>
|
/>
|
||||||
<br />
|
|
||||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||||
|
|
||||||
{#if parameters.confirm}
|
{#if parameters.confirm}
|
||||||
|
@ -161,18 +126,6 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fields">
|
|
||||||
{#key parameters.automationId}
|
|
||||||
<SaveFields
|
|
||||||
schemaFields={selectedSchema}
|
|
||||||
parameterFields={parameters.fields}
|
|
||||||
fieldLabel="Field"
|
|
||||||
on:change={onFieldsChanged}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -184,17 +137,24 @@
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.param-margin {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-padding {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.params {
|
.params {
|
||||||
display: grid;
|
display: flex;
|
||||||
column-gap: var(--spacing-l);
|
flex-wrap: nowrap;
|
||||||
row-gap: var(--spacing-s);
|
gap: 25px;
|
||||||
grid-template-columns: 60px 1fr;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.synchronous-info {
|
.synchronous-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
|
@ -202,29 +162,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
grid-template-columns: 60px 1fr auto 1fr auto;
|
grid-template-columns: 15% auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radios,
|
|
||||||
.radio-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.radios {
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
}
|
|
||||||
.radio-container {
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.radio-container :global(label) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"]:checked {
|
|
||||||
background: var(--blue);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,6 +16,7 @@ export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
export { default as ExportData } from "./ExportData.svelte"
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||||
|
export { default as ScrollTo } from "./ScrollTo.svelte"
|
||||||
export { default as ShowNotification } from "./ShowNotification.svelte"
|
export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||||
export { default as PromptUser } from "./PromptUser.svelte"
|
export { default as PromptUser } from "./PromptUser.svelte"
|
||||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||||
|
|
|
@ -70,6 +70,11 @@
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"component": "UpdateFieldValue"
|
"component": "UpdateFieldValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Scroll To Field",
|
||||||
|
"type": "form",
|
||||||
|
"component": "ScrollTo"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Validate Form",
|
"name": "Validate Form",
|
||||||
"type": "form",
|
"type": "form",
|
||||||
|
|
|
@ -2,9 +2,4 @@
|
||||||
import ColumnEditor from "./ColumnEditor.svelte"
|
import ColumnEditor from "./ColumnEditor.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ColumnEditor
|
<ColumnEditor {...$$props} on:change allowCellEditing={false} />
|
||||||
{...$$props}
|
|
||||||
on:change
|
|
||||||
allowCellEditing={false}
|
|
||||||
subject="Dynamic Filter"
|
|
||||||
/>
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let options = []
|
export let options = []
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let allowCellEditing = true
|
export let allowCellEditing = true
|
||||||
|
export let allowReorder = true
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
let dragDisabled = true
|
let dragDisabled = true
|
||||||
|
@ -110,6 +111,7 @@
|
||||||
{#each columns as column (column.id)}
|
{#each columns as column (column.id)}
|
||||||
<div class="column" animate:flip={{ duration: flipDurationMs }}>
|
<div class="column" animate:flip={{ duration: flipDurationMs }}>
|
||||||
<div
|
<div
|
||||||
|
class:hide={!allowReorder}
|
||||||
class="handle"
|
class="handle"
|
||||||
aria-label="drag-handle"
|
aria-label="drag-handle"
|
||||||
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
|
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
|
||||||
|
@ -142,10 +144,10 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="wide">
|
<div class="wide">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
By default, all table columns will automatically be shown.
|
By default, all columns will automatically be shown.
|
||||||
<br />
|
<br />
|
||||||
You can manually control which columns are included in your table,
|
You can manually control which columns are included by adding them
|
||||||
and their appearance, by adding them below.
|
below.
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,6 +195,9 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
.handle.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
.wide {
|
.wide {
|
||||||
grid-column: 2 / -1;
|
grid-column: 2 / -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value = []
|
export let value = []
|
||||||
export let allowCellEditing = true
|
export let allowCellEditing = true
|
||||||
export let subject = "Table"
|
export let allowReorder = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -75,11 +75,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
<div class="column-editor">
|
||||||
<Drawer bind:this={drawer} title="{subject} Columns">
|
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||||
<svelte:fragment slot="description">
|
</div>
|
||||||
Configure the columns in your {subject.toLowerCase()}.
|
<Drawer bind:this={drawer} title="Columns">
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
<ColumnDrawer
|
<ColumnDrawer
|
||||||
slot="body"
|
slot="body"
|
||||||
|
@ -87,5 +86,12 @@
|
||||||
{options}
|
{options}
|
||||||
{schema}
|
{schema}
|
||||||
{allowCellEditing}
|
{allowCellEditing}
|
||||||
|
{allowReorder}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.column-editor :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import ColumnEditor from "./ColumnEditor.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ColumnEditor
|
||||||
|
{...$$props}
|
||||||
|
on:change
|
||||||
|
allowCellEditing={false}
|
||||||
|
allowReorder={false}
|
||||||
|
/>
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import ColumnDrawer from "./ColumnDrawer.svelte"
|
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
getDatasourceForProvider,
|
getDatasourceForProvider,
|
||||||
|
|
|
@ -20,15 +20,26 @@
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
$: text = getText(value)
|
||||||
|
|
||||||
async function saveFilter() {
|
async function saveFilter() {
|
||||||
dispatch("change", tempValue)
|
dispatch("change", tempValue)
|
||||||
notifications.success("Filters saved")
|
notifications.success("Filters saved")
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getText = filters => {
|
||||||
|
if (!filters?.length) {
|
||||||
|
return "No filters set"
|
||||||
|
} else {
|
||||||
|
return `${filters.length} filter${filters.length === 1 ? "" : "s"} set`
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<div class="filter-editor">
|
||||||
|
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||||
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<FilterDrawer
|
||||||
|
@ -40,3 +51,9 @@
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.filter-editor :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
name: "Sidebar with Main",
|
name: "Sidebar with Main",
|
||||||
icon: "ColumnTwoC",
|
icon: "ColumnTwoC",
|
||||||
},
|
},
|
||||||
|
oneColumn: {
|
||||||
|
name: "One column",
|
||||||
|
icon: "LoupeView",
|
||||||
|
},
|
||||||
twoColumns: {
|
twoColumns: {
|
||||||
name: "Two columns",
|
name: "Two columns",
|
||||||
icon: "ColumnTwoA",
|
icon: "ColumnTwoA",
|
||||||
|
|
|
@ -176,7 +176,10 @@
|
||||||
notifications.success(`Request saved successfully`)
|
notifications.success(`Request saved successfully`)
|
||||||
if (dynamicVariables) {
|
if (dynamicVariables) {
|
||||||
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
||||||
datasource = await datasources.save(datasource)
|
datasource = await datasources.update({
|
||||||
|
integration: integrationInfo,
|
||||||
|
datasource,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
prettifyQueryRequestBody(
|
prettifyQueryRequestBody(
|
||||||
query,
|
query,
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const TOUR_STEP_KEYS = {
|
||||||
BUILDER_DATA_SECTION: "builder-data-section",
|
BUILDER_DATA_SECTION: "builder-data-section",
|
||||||
BUILDER_DESIGN_SECTION: "builder-design-section",
|
BUILDER_DESIGN_SECTION: "builder-design-section",
|
||||||
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
||||||
BUILDER_AUTOMATE_SECTION: "builder-automate-section",
|
BUILDER_AUTOMATION_SECTION: "builder-automation-section",
|
||||||
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const getTours = () => {
|
||||||
title: "Data",
|
title: "Data",
|
||||||
route: "/builder/app/:application/data",
|
route: "/builder/app/:application/data",
|
||||||
layout: OnboardingData,
|
layout: OnboardingData,
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-data-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||||
onLoad: async () => {
|
onLoad: async () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||||
},
|
},
|
||||||
|
@ -45,20 +45,20 @@ const getTours = () => {
|
||||||
title: "Design",
|
title: "Design",
|
||||||
route: "/builder/app/:application/design",
|
route: "/builder/app/:application/design",
|
||||||
layout: OnboardingDesign,
|
layout: OnboardingDesign,
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-design-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
},
|
},
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION,
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||||
title: "Automations",
|
title: "Automations",
|
||||||
route: "/builder/app/:application/automate",
|
route: "/builder/app/:application/automation",
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-automate-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||||
},
|
},
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,18 +4,27 @@
|
||||||
export let active = false
|
export let active = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a on:click href={url} class:active>
|
{#if url}
|
||||||
{text || ""}
|
<a on:click href={url} class:active>
|
||||||
</a>
|
{text || ""}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span on:click class:active>
|
||||||
|
{text || ""}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a {
|
a,
|
||||||
|
span {
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 130ms ease-out;
|
transition: background 130ms ease-out;
|
||||||
}
|
}
|
||||||
.active,
|
.active,
|
||||||
|
span:hover,
|
||||||
a:hover {
|
a:hover {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToOverview = () => {
|
const goToOverview = () => {
|
||||||
$goto(`../overview/${app.devId}`)
|
$goto(`../../app/${app.devId}/settings`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
|
import { lowercase } from "helpers"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
|
|
||||||
const values = writable({ name: "", url: null })
|
const values = writable({ name: "", url: null })
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
|
const encryptionValidation = createValidationStore()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const { url } = $values
|
const { url } = $values
|
||||||
|
@ -27,8 +29,11 @@
|
||||||
...$values,
|
...$values,
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
})
|
})
|
||||||
|
encryptionValidation.check({ ...$values })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: encryptedFile = $values.file?.name?.endsWith(".enc.tar.gz")
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const lastChar = $auth.user?.firstName
|
const lastChar = $auth.user?.firstName
|
||||||
? $auth.user?.firstName[$auth.user?.firstName.length - 1]
|
? $auth.user?.firstName[$auth.user?.firstName.length - 1]
|
||||||
|
@ -87,6 +92,9 @@
|
||||||
appValidation.name(validation, { apps: applications })
|
appValidation.name(validation, { apps: applications })
|
||||||
appValidation.url(validation, { apps: applications })
|
appValidation.url(validation, { apps: applications })
|
||||||
appValidation.file(validation, { template })
|
appValidation.file(validation, { template })
|
||||||
|
|
||||||
|
encryptionValidation.addValidatorType("encryptionPassword", "text", true)
|
||||||
|
|
||||||
// init validation
|
// init validation
|
||||||
const { url } = $values
|
const { url } = $values
|
||||||
validation.check({
|
validation.check({
|
||||||
|
@ -110,6 +118,9 @@
|
||||||
data.append("templateName", template.name)
|
data.append("templateName", template.name)
|
||||||
data.append("templateKey", template.key)
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", $values.file)
|
data.append("templateFile", $values.file)
|
||||||
|
if ($values.encryptionPassword?.trim()) {
|
||||||
|
data.append("encryptionPassword", $values.encryptionPassword.trim())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create App
|
// Create App
|
||||||
|
@ -143,67 +154,119 @@
|
||||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
creating = false
|
creating = false
|
||||||
console.error(error)
|
throw error
|
||||||
notifications.error("Error creating app")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Step = { CONFIG: "config", SET_PASSWORD: "set_password" }
|
||||||
|
let currentStep = Step.CONFIG
|
||||||
|
$: stepConfig = {
|
||||||
|
[Step.CONFIG]: {
|
||||||
|
title: "Create your app",
|
||||||
|
confirmText: template?.fromFile ? "Import app" : "Create app",
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (encryptedFile) {
|
||||||
|
currentStep = Step.SET_PASSWORD
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await createNewApp()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error creating app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValid: $validation.valid,
|
||||||
|
},
|
||||||
|
[Step.SET_PASSWORD]: {
|
||||||
|
title: "Provide the export password",
|
||||||
|
confirmText: "Import app",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await createNewApp()
|
||||||
|
} catch (e) {
|
||||||
|
let message = "Error creating app"
|
||||||
|
if (e.message) {
|
||||||
|
message += `: ${lowercase(e.message)}`
|
||||||
|
}
|
||||||
|
notifications.error(message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValid: $encryptionValidation.valid,
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={"Create your app"}
|
title={stepConfig[currentStep].title}
|
||||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
confirmText={stepConfig[currentStep].confirmText}
|
||||||
onConfirm={createNewApp}
|
onConfirm={stepConfig[currentStep].onConfirm}
|
||||||
disabled={!$validation.valid}
|
disabled={!stepConfig[currentStep].isValid}
|
||||||
>
|
>
|
||||||
{#if template && !template?.fromFile}
|
{#if currentStep === Step.CONFIG}
|
||||||
<TemplateCard
|
{#if template && !template?.fromFile}
|
||||||
name={template.name}
|
<TemplateCard
|
||||||
imageSrc={template.image}
|
name={template.name}
|
||||||
backgroundColour={template.background}
|
imageSrc={template.image}
|
||||||
overlayEnabled={false}
|
backgroundColour={template.background}
|
||||||
icon={template.icon}
|
overlayEnabled={false}
|
||||||
/>
|
icon={template.icon}
|
||||||
{/if}
|
/>
|
||||||
{#if template?.fromFile}
|
|
||||||
<Dropzone
|
|
||||||
error={$validation.touched.file && $validation.errors.file}
|
|
||||||
gallery={false}
|
|
||||||
label="File to import"
|
|
||||||
value={[$values.file]}
|
|
||||||
on:change={e => {
|
|
||||||
$values.file = e.detail?.[0]
|
|
||||||
$validation.touched.file = true
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<Input
|
|
||||||
autofocus={true}
|
|
||||||
bind:value={$values.name}
|
|
||||||
disabled={creating}
|
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
|
||||||
on:change={nameToUrl($values.name)}
|
|
||||||
label="Name"
|
|
||||||
placeholder={defaultAppName}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<Input
|
|
||||||
bind:value={$values.url}
|
|
||||||
disabled={creating}
|
|
||||||
error={$validation.touched.url && $validation.errors.url}
|
|
||||||
on:blur={() => ($validation.touched.url = true)}
|
|
||||||
on:change={tidyUrl($values.url)}
|
|
||||||
label="URL"
|
|
||||||
placeholder={$values.url
|
|
||||||
? $values.url
|
|
||||||
: `/${resolveAppUrl(template, $values.name)}`}
|
|
||||||
/>
|
|
||||||
{#if $values.url && $values.url !== "" && !$validation.errors.url}
|
|
||||||
<div class="app-server" title={appUrl}>
|
|
||||||
{appUrl}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
{#if template?.fromFile}
|
||||||
|
<Dropzone
|
||||||
|
error={$validation.touched.file && $validation.errors.file}
|
||||||
|
gallery={false}
|
||||||
|
label="File to import"
|
||||||
|
value={[$values.file]}
|
||||||
|
on:change={e => {
|
||||||
|
$values.file = e.detail?.[0]
|
||||||
|
$validation.touched.file = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Input
|
||||||
|
autofocus={true}
|
||||||
|
bind:value={$values.name}
|
||||||
|
disabled={creating}
|
||||||
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
|
on:change={nameToUrl($values.name)}
|
||||||
|
label="Name"
|
||||||
|
placeholder={defaultAppName}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
disabled={creating}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
on:change={tidyUrl($values.url)}
|
||||||
|
label="URL"
|
||||||
|
placeholder={$values.url
|
||||||
|
? $values.url
|
||||||
|
: `/${resolveAppUrl(template, $values.name)}`}
|
||||||
|
/>
|
||||||
|
{#if $values.url && $values.url !== "" && !$validation.errors.url}
|
||||||
|
<div class="app-server" title={appUrl}>
|
||||||
|
{appUrl}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if currentStep === Step.SET_PASSWORD}
|
||||||
|
<Input
|
||||||
|
autofocus={true}
|
||||||
|
label="Imported file password"
|
||||||
|
type="password"
|
||||||
|
bind:value={$values.encryptionPassword}
|
||||||
|
disabled={creating}
|
||||||
|
on:blur={() => ($encryptionValidation.touched.encryptionPassword = true)}
|
||||||
|
error={$encryptionValidation.touched.encryptionPassword &&
|
||||||
|
$encryptionValidation.errors.encryptionPassword}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,27 +1,128 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Toggle, Body, InlineAlert } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Toggle,
|
||||||
|
Body,
|
||||||
|
InlineAlert,
|
||||||
|
Input,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let published
|
export let published
|
||||||
let excludeRows = false
|
let includeInternalTablesRows = true
|
||||||
|
let encypt = true
|
||||||
|
|
||||||
$: title = published ? "Export published app" : "Export latest app"
|
let password = null
|
||||||
$: confirmText = published ? "Export published" : "Export latest"
|
const validation = createValidationStore()
|
||||||
|
validation.addValidatorType("password", "password", true, { minLength: 8 })
|
||||||
|
$: validation.observe("password", password)
|
||||||
|
|
||||||
const exportApp = () => {
|
const Step = { CONFIG: "config", SET_PASSWORD: "set_password" }
|
||||||
|
let currentStep = Step.CONFIG
|
||||||
|
|
||||||
|
$: exportButtonText = published ? "Export published" : "Export latest"
|
||||||
|
$: stepConfig = {
|
||||||
|
[Step.CONFIG]: {
|
||||||
|
title: published ? "Export published app" : "Export latest app",
|
||||||
|
confirmText: encypt ? "Continue" : exportButtonText,
|
||||||
|
onConfirm: () => {
|
||||||
|
if (!encypt) {
|
||||||
|
exportApp()
|
||||||
|
} else {
|
||||||
|
currentStep = Step.SET_PASSWORD
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
[Step.SET_PASSWORD]: {
|
||||||
|
title: "Add password to encrypt your export",
|
||||||
|
confirmText: exportButtonText,
|
||||||
|
onConfirm: async () => {
|
||||||
|
await validation.check({ password })
|
||||||
|
if (!$validation.valid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
exportApp(password)
|
||||||
|
},
|
||||||
|
isValid: $validation.valid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportApp = async () => {
|
||||||
const id = published ? app.prodId : app.devId
|
const id = published ? app.prodId : app.devId
|
||||||
const appName = encodeURIComponent(app.name)
|
const url = `/api/backups/export?appId=${id}`
|
||||||
window.location = `/api/backups/export?appId=${id}&appname=${appName}&excludeRows=${excludeRows}`
|
await downloadFile(url, {
|
||||||
|
excludeRows: !includeInternalTablesRows,
|
||||||
|
encryptPassword: password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(url, body) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const contentDisposition = response.headers.get("Content-Disposition")
|
||||||
|
|
||||||
|
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(
|
||||||
|
contentDisposition
|
||||||
|
)
|
||||||
|
|
||||||
|
const filename = matches[1].replace(/['"]/g, "")
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(await response.blob())
|
||||||
|
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} else {
|
||||||
|
notifications.error("Error exporting the app.")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error.message || "Error downloading the exported app")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
<ModalContent
|
||||||
<InlineAlert
|
title={stepConfig[currentStep].title}
|
||||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
confirmText={stepConfig[currentStep].confirmText}
|
||||||
/>
|
onConfirm={stepConfig[currentStep].onConfirm}
|
||||||
<Body
|
disabled={!stepConfig[currentStep].isValid}
|
||||||
>Apps can be exported with or without data that is within internal tables -
|
>
|
||||||
select this below.</Body
|
{#if currentStep === Step.CONFIG}
|
||||||
>
|
<Body>
|
||||||
<Toggle text="Exclude Rows" bind:value={excludeRows} />
|
<Toggle
|
||||||
|
text="Export rows from internal tables"
|
||||||
|
bind:value={includeInternalTablesRows}
|
||||||
|
/>
|
||||||
|
<Toggle text="Encrypt my export" bind:value={encypt} />
|
||||||
|
</Body>
|
||||||
|
{#if !encypt}
|
||||||
|
<InlineAlert
|
||||||
|
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if currentStep === Step.SET_PASSWORD}
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Type here..."
|
||||||
|
bind:value={password}
|
||||||
|
error={$validation.errors.password}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
import EditableIcon from "../common/EditableIcon.svelte"
|
import EditableIcon from "../common/EditableIcon.svelte"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
export let onUpdateComplete
|
||||||
|
|
||||||
|
$: appIdParts = app.appId ? app.appId?.split("_") : []
|
||||||
|
$: appId = appIdParts.slice(-1)[0]
|
||||||
|
|
||||||
const values = writable({
|
const values = writable({
|
||||||
name: app.name,
|
name: app.name,
|
||||||
|
@ -34,8 +38,20 @@
|
||||||
|
|
||||||
const setupValidation = async () => {
|
const setupValidation = async () => {
|
||||||
const applications = svelteGet(apps)
|
const applications = svelteGet(apps)
|
||||||
appValidation.name(validation, { apps: applications, currentApp: app })
|
appValidation.name(validation, {
|
||||||
appValidation.url(validation, { apps: applications, currentApp: app })
|
apps: applications,
|
||||||
|
currentApp: {
|
||||||
|
...app,
|
||||||
|
appId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
appValidation.url(validation, {
|
||||||
|
apps: applications,
|
||||||
|
currentApp: {
|
||||||
|
...app,
|
||||||
|
appId,
|
||||||
|
},
|
||||||
|
})
|
||||||
// init validation
|
// init validation
|
||||||
const { url } = $values
|
const { url } = $values
|
||||||
validation.check({
|
validation.check({
|
||||||
|
@ -46,7 +62,7 @@
|
||||||
|
|
||||||
async function updateApp() {
|
async function updateApp() {
|
||||||
try {
|
try {
|
||||||
await apps.update(app.instance._id, {
|
await apps.update(app.appId, {
|
||||||
name: $values.name?.trim(),
|
name: $values.name?.trim(),
|
||||||
url: $values.url?.trim(),
|
url: $values.url?.trim(),
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -54,6 +70,9 @@
|
||||||
color: $values.iconColor,
|
color: $values.iconColor,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if (typeof onUpdateComplete == "function") {
|
||||||
|
onUpdateComplete()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error updating app")
|
notifications.error("Error updating app")
|
||||||
|
|
|
@ -6,7 +6,6 @@ export function createValidationStore(initialValue, ...validators) {
|
||||||
let touched = false
|
let touched = false
|
||||||
|
|
||||||
const value = writable(initialValue || "")
|
const value = writable(initialValue || "")
|
||||||
const error = derived(value, $v => validate($v, validators))
|
|
||||||
const touchedStore = derived(value, () => {
|
const touchedStore = derived(value, () => {
|
||||||
if (!touched) {
|
if (!touched) {
|
||||||
touched = true
|
touched = true
|
||||||
|
@ -14,6 +13,10 @@ export function createValidationStore(initialValue, ...validators) {
|
||||||
}
|
}
|
||||||
return touched
|
return touched
|
||||||
})
|
})
|
||||||
|
const error = derived(
|
||||||
|
[value, touchedStore],
|
||||||
|
([$v, $t]) => $t && validate($v, validators)
|
||||||
|
)
|
||||||
|
|
||||||
return [value, error, touchedStore]
|
return [value, error, touchedStore]
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue