Merge branch 'develop' into cypress-testing
This commit is contained in:
commit
ea5d164bdc
|
@ -7,7 +7,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
- new-design-ui
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
@ -60,19 +59,3 @@ jobs:
|
||||||
with:
|
with:
|
||||||
install: false
|
install: false
|
||||||
command: yarn test:e2e:ci
|
command: yarn test:e2e:ci
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: eu-west-1
|
|
||||||
|
|
||||||
- name: Upload to S3
|
|
||||||
if: github.ref == 'refs/heads/new-design-ui'
|
|
||||||
run: |
|
|
||||||
tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html
|
|
||||||
aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/
|
|
||||||
aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js
|
|
||||||
aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*"
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Deploy Budibase Single Container Image to DockerHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||||
|
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||||
|
CI: true
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
REGISTRY_URL: registry.hub.docker.com
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "build"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x]
|
||||||
|
steps:
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
|
- name: Run Yarn
|
||||||
|
run: yarn
|
||||||
|
- name: Run Yarn Bootstrap
|
||||||
|
run: yarn bootstrap
|
||||||
|
- name: Runt Yarn Lint
|
||||||
|
run: yarn lint
|
||||||
|
- name: Run Yarn Build
|
||||||
|
run: yarn build:docker:pre
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
- name: Get the latest release version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
|
echo $release_version
|
||||||
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
- name: Tag and release Budibase service docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
||||||
|
file: ./hosting/single/Dockerfile
|
|
@ -16,6 +16,16 @@ on:
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'yarn.lock'
|
- 'yarn.lock'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
versioning:
|
||||||
|
type: choice
|
||||||
|
description: "Versioning type: patch, minor, major"
|
||||||
|
default: patch
|
||||||
|
options:
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
required: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
|
@ -58,6 +68,7 @@ jobs:
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }}
|
||||||
run: |
|
run: |
|
||||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||||
git config --global user.name "Budibase Release Bot"
|
git config --global user.name "Budibase Release Bot"
|
||||||
|
|
|
@ -135,13 +135,18 @@ You can learn more about the Budibase API at the following places:
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
||||||
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
|
||||||
|
|
||||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||||
|
|
||||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||||
|
|
||||||
|
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
|
||||||
|
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
|
||||||
|
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
|
||||||
|
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||||
|
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||||
|
|
||||||
|
|
||||||
### [Get started with Budibase Cloud](https://budibase.com)
|
### [Get started with Budibase Cloud](https://budibase.com)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,14 @@ spec:
|
||||||
value: {{ .Values.globals.automationMaxIterations | quote }}
|
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||||
- name: TENANT_FEATURE_FLAGS
|
- name: TENANT_FEATURE_FLAGS
|
||||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||||
|
{{ if .Values.globals.bbAdminUserEmail }}
|
||||||
|
- name: BB_ADMIN_USER_EMAIL
|
||||||
|
value: { { .Values.globals.bbAdminUserEmail | quote } }
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.bbAdminUserPassword }}
|
||||||
|
- name: BB_ADMIN_USER_PASSWORD
|
||||||
|
value: { { .Values.globals.bbAdminUserPassword | quote } }
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
|
|
|
@ -18,4 +18,8 @@ MINIO_PORT=4004
|
||||||
COUCH_DB_PORT=4005
|
COUCH_DB_PORT=4005
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
WATCHTOWER_PORT=6161
|
WATCHTOWER_PORT=6161
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION
|
BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
|
||||||
|
# An admin user can be automatically created initially if these are set
|
||||||
|
BB_ADMIN_USER_EMAIL=
|
||||||
|
BB_ADMIN_USER_PASSWORD=
|
|
@ -23,6 +23,8 @@ services:
|
||||||
ENABLE_ANALYTICS: "true"
|
ENABLE_ANALYTICS: "true"
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||||
|
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-service
|
- worker-service
|
||||||
- redis-service
|
- redis-service
|
||||||
|
|
|
@ -19,3 +19,7 @@ COUCH_DB_PORT=4005
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
WATCHTOWER_PORT=6161
|
WATCHTOWER_PORT=6161
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION
|
BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
|
||||||
|
# An admin user can be automatically created initially if these are set
|
||||||
|
BB_ADMIN_USER_EMAIL=
|
||||||
|
BB_ADMIN_USER_PASSWORD=
|
|
@ -34,27 +34,32 @@ ENV \
|
||||||
ARCHITECTURE=amd \
|
ARCHITECTURE=amd \
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION \
|
BUDIBASE_ENVIRONMENT=PRODUCTION \
|
||||||
CLUSTER_PORT=80 \
|
CLUSTER_PORT=80 \
|
||||||
COUCHDB_PASSWORD=budibase \
|
|
||||||
COUCHDB_USER=budibase \
|
|
||||||
COUCH_DB_URL=http://budibase:budibase@localhost:5984 \
|
|
||||||
# CUSTOM_DOMAIN=budi001.custom.com \
|
# CUSTOM_DOMAIN=budi001.custom.com \
|
||||||
DEPLOYMENT_ENVIRONMENT=docker \
|
DEPLOYMENT_ENVIRONMENT=docker \
|
||||||
INTERNAL_API_KEY=budibase \
|
|
||||||
JWT_SECRET=testsecret \
|
|
||||||
MINIO_ACCESS_KEY=budibase \
|
|
||||||
MINIO_SECRET_KEY=budibase \
|
|
||||||
MINIO_URL=http://localhost:9000 \
|
MINIO_URL=http://localhost:9000 \
|
||||||
POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS \
|
POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS \
|
||||||
REDIS_PASSWORD=budibase \
|
|
||||||
REDIS_URL=localhost:6379 \
|
REDIS_URL=localhost:6379 \
|
||||||
SELF_HOSTED=1 \
|
SELF_HOSTED=1 \
|
||||||
TARGETBUILD=$TARGETBUILD \
|
TARGETBUILD=$TARGETBUILD \
|
||||||
WORKER_PORT=4002 \
|
WORKER_PORT=4002 \
|
||||||
WORKER_URL=http://localhost:4002
|
WORKER_URL=http://localhost:4002 \
|
||||||
|
APPS_URL=http://localhost:4001
|
||||||
|
|
||||||
|
# These secret env variables are generated by the runner at startup
|
||||||
|
# their values can be overriden by the user, they will be written
|
||||||
|
# to the .env file in the /data directory for use later on
|
||||||
|
# REDIS_PASSWORD=budibase \
|
||||||
|
# COUCHDB_PASSWORD=budibase \
|
||||||
|
# COUCHDB_USER=budibase \
|
||||||
|
# COUCH_DB_URL=http://budibase:budibase@localhost:5984 \
|
||||||
|
# INTERNAL_API_KEY=budibase \
|
||||||
|
# JWT_SECRET=testsecret \
|
||||||
|
# MINIO_ACCESS_KEY=budibase \
|
||||||
|
# MINIO_SECRET_KEY=budibase \
|
||||||
|
|
||||||
# install base dependencies
|
# install base dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y software-properties-common wget nginx && \
|
apt-get install -y software-properties-common wget nginx uuid-runtime && \
|
||||||
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
|
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
|
||||||
apt-get update
|
apt-get update
|
||||||
|
|
||||||
|
@ -66,8 +71,8 @@ RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh &
|
||||||
npm install --global yarn pm2
|
npm install --global yarn pm2
|
||||||
|
|
||||||
# setup nginx
|
# setup nginx
|
||||||
ADD hosting/single/nginx.conf /etc/nginx
|
ADD hosting/single/nginx/nginx.conf /etc/nginx
|
||||||
ADD hosting/single/nginx-default-site.conf /etc/nginx/sites-enabled/default
|
ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
|
||||||
RUN mkdir -p /var/log/nginx && \
|
RUN mkdir -p /var/log/nginx && \
|
||||||
touch /var/log/nginx/error.log && \
|
touch /var/log/nginx/error.log && \
|
||||||
touch /var/run/nginx.pid
|
touch /var/run/nginx.pid
|
||||||
|
@ -86,13 +91,13 @@ RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clou
|
||||||
|
|
||||||
WORKDIR /opt/clouseau
|
WORKDIR /opt/clouseau
|
||||||
RUN mkdir ./bin
|
RUN mkdir ./bin
|
||||||
ADD hosting/single/clouseau ./bin/
|
ADD hosting/single/clouseau/clouseau ./bin/
|
||||||
ADD hosting/single/log4j.properties hosting/single/clouseau.ini ./
|
ADD hosting/single/clouseau/log4j.properties hosting/single/clouseau/clouseau.ini ./
|
||||||
RUN chmod +x ./bin/clouseau
|
RUN chmod +x ./bin/clouseau
|
||||||
|
|
||||||
# setup CouchDB
|
# setup CouchDB
|
||||||
WORKDIR /opt/couchdb
|
WORKDIR /opt/couchdb
|
||||||
ADD hosting/single/vm.args ./etc/
|
ADD hosting/single/couch/vm.args hosting/single/couch/local.ini ./etc/
|
||||||
|
|
||||||
# setup minio
|
# setup minio
|
||||||
WORKDIR /minio
|
WORKDIR /minio
|
||||||
|
|
|
@ -7,7 +7,7 @@ name=clouseau@127.0.0.1
|
||||||
cookie=monster
|
cookie=monster
|
||||||
|
|
||||||
; the path where you would like to store the search index files
|
; the path where you would like to store the search index files
|
||||||
dir=/opt/couchdb/data/search
|
dir=/data/search
|
||||||
|
|
||||||
; the number of search indexes that can be open simultaneously
|
; the number of search indexes that can be open simultaneously
|
||||||
max_indexes_open=500
|
max_indexes_open=500
|
|
@ -0,0 +1,5 @@
|
||||||
|
; CouchDB Configuration Settings
|
||||||
|
|
||||||
|
[couchdb]
|
||||||
|
database_dir = /data/couch/dbs
|
||||||
|
view_index_dir = /data/couch/views
|
|
@ -88,7 +88,4 @@ server {
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
||||||
|
if [ -f "/data/.env" ]; then
|
||||||
|
export $(cat /data/.env | xargs)
|
||||||
|
fi
|
||||||
|
# first randomise any unset environment variables
|
||||||
|
for ENV_VAR in "${ENV_VARS[@]}"
|
||||||
|
do
|
||||||
|
temp=$(eval "echo \$$ENV_VAR")
|
||||||
|
if [[ -z "${temp}" ]]; then
|
||||||
|
eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z "${COUCH_DB_URL}" ]]; then
|
||||||
|
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||||
|
fi
|
||||||
|
if [ ! -f "/data/.env" ]; then
|
||||||
|
touch /data/.env
|
||||||
|
for ENV_VAR in "${ENV_VARS[@]}"
|
||||||
|
do
|
||||||
|
temp=$(eval "echo \$$ENV_VAR")
|
||||||
|
echo "$ENV_VAR=$temp" >> /data/.env
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# make these directories in runner, incase of mount
|
||||||
|
mkdir -p /data/couch/dbs /data/couch/views
|
||||||
|
chown couchdb:couchdb /data/couch /data/couch/dbs /data/couch/views
|
||||||
redis-server --requirepass $REDIS_PASSWORD &
|
redis-server --requirepass $REDIS_PASSWORD &
|
||||||
/opt/clouseau/bin/clouseau &
|
/opt/clouseau/bin/clouseau &
|
||||||
/minio/minio server /minio &
|
/minio/minio server /data/minio &
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
/etc/init.d/nginx restart
|
/etc/init.d/nginx restart
|
||||||
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
id=$(docker run -t -d -p 80:80 budibase:latest)
|
id=$(docker run -t -d -p 8080:80 budibase:latest)
|
||||||
docker exec -it $id bash
|
docker exec -it $id bash
|
||||||
docker kill $id
|
docker kill $id
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.219-alpha.13",
|
"version": "1.1.10-alpha.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
|
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||||
"release:pro": "bash scripts/pro/release.sh",
|
"release:pro": "bash scripts/pro/release.sh",
|
||||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||||
|
@ -40,7 +40,8 @@
|
||||||
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test && yarn test:pro",
|
||||||
|
"test:pro": "bash scripts/pro/test.sh",
|
||||||
"lint:eslint": "eslint packages",
|
"lint:eslint": "eslint packages",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
"test:e2e:ci:notify": "lerna run cy:ci:notify",
|
"test:e2e:ci:notify": "lerna run cy:ci:notify",
|
||||||
"build:specs": "lerna run specs",
|
"build:specs": "lerna run specs",
|
||||||
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||||
|
"build:docker:pre": "lerna run build && lerna run predocker",
|
||||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||||
|
@ -64,7 +66,7 @@
|
||||||
"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 .",
|
||||||
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
|
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
|
||||||
"build:docs": "lerna run build:docs",
|
"build:docs": "lerna run build:docs",
|
||||||
"release:helm": "node scripts/releaseHelmChart",
|
"release:helm": "node scripts/releaseHelmChart",
|
||||||
"env:multi:enable": "lerna run env:multi:enable",
|
"env:multi:enable": "lerna run env:multi:enable",
|
||||||
|
|
|
@ -5,4 +5,5 @@ module.exports = {
|
||||||
app: require("./src/cache/appMetadata"),
|
app: require("./src/cache/appMetadata"),
|
||||||
writethrough: require("./src/cache/writethrough"),
|
writethrough: require("./src/cache/writethrough"),
|
||||||
...generic,
|
...generic,
|
||||||
|
cache: generic,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.219-alpha.13",
|
"version": "1.1.10-alpha.4",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "^1.0.219-alpha.13",
|
"@budibase/types": "^1.1.10-alpha.4",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
|
@ -62,6 +62,7 @@
|
||||||
"@shopify/jest-koa-mocks": "3.1.5",
|
"@shopify/jest-koa-mocks": "3.1.5",
|
||||||
"@types/jest": "27.5.1",
|
"@types/jest": "27.5.1",
|
||||||
"@types/koa": "2.0.52",
|
"@types/koa": "2.0.52",
|
||||||
|
"@types/lodash": "4.14.180",
|
||||||
"@types/node": "14.18.20",
|
"@types/node": "14.18.20",
|
||||||
"@types/node-fetch": "2.6.1",
|
"@types/node-fetch": "2.6.1",
|
||||||
"@types/pouchdb": "6.4.0",
|
"@types/pouchdb": "6.4.0",
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
export enum ContextKeys {
|
||||||
|
TENANT_ID = "tenantId",
|
||||||
|
GLOBAL_DB = "globalDb",
|
||||||
|
APP_ID = "appId",
|
||||||
|
IDENTITY = "identity",
|
||||||
|
// whatever the request app DB was
|
||||||
|
CURRENT_DB = "currentDb",
|
||||||
|
// get the prod app DB from the request
|
||||||
|
PROD_DB = "prodDb",
|
||||||
|
// get the dev app DB from the request
|
||||||
|
DEV_DB = "devDb",
|
||||||
|
DB_OPTS = "dbOpts",
|
||||||
|
// check if something else is using the context, don't close DB
|
||||||
|
TENANCY_IN_USE = "tenancyInUse",
|
||||||
|
APP_IN_USE = "appInUse",
|
||||||
|
IDENTITY_IN_USE = "identityInUse",
|
||||||
|
}
|
|
@ -1,354 +0,0 @@
|
||||||
const env = require("../environment")
|
|
||||||
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
|
||||||
const { DEFAULT_TENANT_ID } = require("../constants")
|
|
||||||
const cls = require("./FunctionContext")
|
|
||||||
const { dangerousGetDB, closeDB } = require("../db")
|
|
||||||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
|
||||||
const { baseGlobalDBName } = require("../tenancy/utils")
|
|
||||||
const { isEqual } = require("lodash")
|
|
||||||
|
|
||||||
// some test cases call functions directly, need to
|
|
||||||
// store an app ID to pretend there is a context
|
|
||||||
let TEST_APP_ID = null
|
|
||||||
|
|
||||||
const ContextKeys = {
|
|
||||||
TENANT_ID: "tenantId",
|
|
||||||
GLOBAL_DB: "globalDb",
|
|
||||||
APP_ID: "appId",
|
|
||||||
IDENTITY: "identity",
|
|
||||||
// whatever the request app DB was
|
|
||||||
CURRENT_DB: "currentDb",
|
|
||||||
// get the prod app DB from the request
|
|
||||||
PROD_DB: "prodDb",
|
|
||||||
// get the dev app DB from the request
|
|
||||||
DEV_DB: "devDb",
|
|
||||||
DB_OPTS: "dbOpts",
|
|
||||||
// check if something else is using the context, don't close DB
|
|
||||||
IN_USE: "inUse",
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
|
|
||||||
|
|
||||||
// this function makes sure the PouchDB objects are closed and
|
|
||||||
// fully deleted when finished - this protects against memory leaks
|
|
||||||
async function closeAppDBs() {
|
|
||||||
const dbKeys = [
|
|
||||||
ContextKeys.CURRENT_DB,
|
|
||||||
ContextKeys.PROD_DB,
|
|
||||||
ContextKeys.DEV_DB,
|
|
||||||
]
|
|
||||||
for (let dbKey of dbKeys) {
|
|
||||||
const db = cls.getFromContext(dbKey)
|
|
||||||
if (!db) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await closeDB(db)
|
|
||||||
// clear the DB from context, incase someone tries to use it again
|
|
||||||
cls.setOnContext(dbKey, null)
|
|
||||||
}
|
|
||||||
// clear the app ID now that the databases are closed
|
|
||||||
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
|
||||||
cls.setOnContext(ContextKeys.APP_ID, null)
|
|
||||||
}
|
|
||||||
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
|
||||||
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.closeTenancy = async () => {
|
|
||||||
if (env.USE_COUCH) {
|
|
||||||
await closeDB(exports.getGlobalDB())
|
|
||||||
}
|
|
||||||
// clear from context now that database is closed/task is finished
|
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
|
||||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isDefaultTenant = () => {
|
|
||||||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isMultiTenant = () => {
|
|
||||||
return env.MULTI_TENANCY
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for automations, API endpoints should always be in context already
|
|
||||||
exports.doInTenant = (tenantId, task, { forceNew } = {}) => {
|
|
||||||
// the internal function is so that we can re-use an existing
|
|
||||||
// context - don't want to close DB on a parent context
|
|
||||||
async function internal(opts = { existing: false }) {
|
|
||||||
// set the tenant id
|
|
||||||
if (!opts.existing) {
|
|
||||||
exports.updateTenantId(tenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// invoke the task
|
|
||||||
return await task()
|
|
||||||
} finally {
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (!using || using <= 1) {
|
|
||||||
await exports.closeTenancy()
|
|
||||||
} else {
|
|
||||||
cls.setOnContext(using - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (
|
|
||||||
!forceNew &&
|
|
||||||
using &&
|
|
||||||
cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
|
|
||||||
) {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
|
||||||
return internal({ existing: true })
|
|
||||||
} else {
|
|
||||||
return cls.run(async () => {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
|
||||||
return internal()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
|
||||||
* @return {null|string} The tenant ID found within the app ID.
|
|
||||||
*/
|
|
||||||
exports.getTenantIDFromAppID = appId => {
|
|
||||||
if (!appId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const split = appId.split(SEPARATOR)
|
|
||||||
const hasDev = split[1] === DocumentTypes.DEV
|
|
||||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (hasDev) {
|
|
||||||
return split[2]
|
|
||||||
} else {
|
|
||||||
return split[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAppTenantId = appId => {
|
|
||||||
const appTenantId =
|
|
||||||
exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID
|
|
||||||
exports.updateTenantId(appTenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.doInAppContext = (appId, task, { forceNew } = {}) => {
|
|
||||||
if (!appId) {
|
|
||||||
throw new Error("appId is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
const identity = exports.getIdentity()
|
|
||||||
|
|
||||||
// the internal function is so that we can re-use an existing
|
|
||||||
// context - don't want to close DB on a parent context
|
|
||||||
async function internal(opts = { existing: false }) {
|
|
||||||
// set the app tenant id
|
|
||||||
if (!opts.existing) {
|
|
||||||
setAppTenantId(appId)
|
|
||||||
}
|
|
||||||
// set the app ID
|
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
|
||||||
// preserve the identity
|
|
||||||
exports.setIdentity(identity)
|
|
||||||
try {
|
|
||||||
// invoke the task
|
|
||||||
return await task()
|
|
||||||
} finally {
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (!using || using <= 1) {
|
|
||||||
await closeAppDBs()
|
|
||||||
} else {
|
|
||||||
cls.setOnContext(using - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
|
||||||
return internal({ existing: true })
|
|
||||||
} else {
|
|
||||||
return cls.run(async () => {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
|
||||||
return internal()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.doInIdentityContext = (identity, task) => {
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("identity is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function internal(opts = { existing: false }) {
|
|
||||||
if (!opts.existing) {
|
|
||||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
|
||||||
// set the tenant so that doInTenant will preserve identity
|
|
||||||
if (identity.tenantId) {
|
|
||||||
exports.updateTenantId(identity.tenantId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// invoke the task
|
|
||||||
return await task()
|
|
||||||
} finally {
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (!using || using <= 1) {
|
|
||||||
exports.setIdentity(null)
|
|
||||||
} else {
|
|
||||||
cls.setOnContext(using - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = cls.getFromContext(ContextKeys.IDENTITY)
|
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
||||||
if (using && existing && existing._id === identity._id) {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
|
||||||
return internal({ existing: true })
|
|
||||||
} else {
|
|
||||||
return cls.run(async () => {
|
|
||||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
|
||||||
return internal({ existing: false })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setIdentity = identity => {
|
|
||||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getIdentity = () => {
|
|
||||||
try {
|
|
||||||
return cls.getFromContext(ContextKeys.IDENTITY)
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing - identity is not in context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateTenantId = tenantId => {
|
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
|
||||||
if (env.USE_COUCH) {
|
|
||||||
exports.setGlobalDB(tenantId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateAppId = async appId => {
|
|
||||||
try {
|
|
||||||
// have to close first, before removing the databases from context
|
|
||||||
await closeAppDBs()
|
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
|
||||||
} catch (err) {
|
|
||||||
if (env.isTest()) {
|
|
||||||
TEST_APP_ID = appId
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setGlobalDB = tenantId => {
|
|
||||||
const dbName = baseGlobalDBName(tenantId)
|
|
||||||
const db = dangerousGetDB(dbName)
|
|
||||||
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getGlobalDB = () => {
|
|
||||||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
|
||||||
if (!db) {
|
|
||||||
throw new Error("Global DB not found")
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isTenantIdSet = () => {
|
|
||||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
|
||||||
return !!tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getTenantId = () => {
|
|
||||||
if (!exports.isMultiTenant()) {
|
|
||||||
return exports.DEFAULT_TENANT_ID
|
|
||||||
}
|
|
||||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
|
||||||
if (!tenantId) {
|
|
||||||
throw new Error("Tenant id not found")
|
|
||||||
}
|
|
||||||
return tenantId
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAppId = () => {
|
|
||||||
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
|
||||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
|
||||||
return TEST_APP_ID
|
|
||||||
} else {
|
|
||||||
return foundId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContextDB(key, opts) {
|
|
||||||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
|
||||||
let storedOpts = cls.getFromContext(dbOptsKey)
|
|
||||||
let db = cls.getFromContext(key)
|
|
||||||
if (db && isEqual(opts, storedOpts)) {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
const appId = exports.getAppId()
|
|
||||||
let toUseAppId
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case ContextKeys.CURRENT_DB:
|
|
||||||
toUseAppId = appId
|
|
||||||
break
|
|
||||||
case ContextKeys.PROD_DB:
|
|
||||||
toUseAppId = getProdAppID(appId)
|
|
||||||
break
|
|
||||||
case ContextKeys.DEV_DB:
|
|
||||||
toUseAppId = getDevelopmentAppID(appId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
db = dangerousGetDB(toUseAppId, opts)
|
|
||||||
try {
|
|
||||||
cls.setOnContext(key, db)
|
|
||||||
if (opts) {
|
|
||||||
cls.setOnContext(dbOptsKey, opts)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!env.isTest()) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the app database based on whatever the request
|
|
||||||
* contained, dev or prod.
|
|
||||||
*/
|
|
||||||
exports.getAppDB = (opts = null) => {
|
|
||||||
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This specifically gets the prod app ID, if the request
|
|
||||||
* contained a development app ID, this will open the prod one.
|
|
||||||
*/
|
|
||||||
exports.getProdAppDB = (opts = null) => {
|
|
||||||
return getContextDB(ContextKeys.PROD_DB, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This specifically gets the dev app ID, if the request
|
|
||||||
* contained a prod app ID, this will open the dev one.
|
|
||||||
*/
|
|
||||||
exports.getDevAppDB = (opts = null) => {
|
|
||||||
return getContextDB(ContextKeys.DEV_DB, opts)
|
|
||||||
}
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
import env from "../environment"
|
||||||
|
import { SEPARATOR, DocumentTypes } from "../db/constants"
|
||||||
|
import cls from "./FunctionContext"
|
||||||
|
import { dangerousGetDB, closeDB } from "../db"
|
||||||
|
import { baseGlobalDBName } from "../tenancy/utils"
|
||||||
|
import { IdentityContext } from "@budibase/types"
|
||||||
|
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
|
||||||
|
import { ContextKeys } from "./constants"
|
||||||
|
import {
|
||||||
|
updateUsing,
|
||||||
|
closeWithUsing,
|
||||||
|
setAppTenantId,
|
||||||
|
setIdentity,
|
||||||
|
closeAppDBs,
|
||||||
|
getContextDB,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
|
||||||
|
|
||||||
|
// some test cases call functions directly, need to
|
||||||
|
// store an app ID to pretend there is a context
|
||||||
|
let TEST_APP_ID: string | null = null
|
||||||
|
|
||||||
|
export const closeTenancy = async () => {
|
||||||
|
let db
|
||||||
|
try {
|
||||||
|
if (env.USE_COUCH) {
|
||||||
|
db = getGlobalDB()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// no DB found - skip closing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await closeDB(db)
|
||||||
|
// clear from context now that database is closed/task is finished
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||||
|
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const isDefaultTenant = () => {
|
||||||
|
// return getTenantId() === DEFAULT_TENANT_ID
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const isMultiTenant = () => {
|
||||||
|
return env.MULTI_TENANCY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||||
|
* @return {null|string} The tenant ID found within the app ID.
|
||||||
|
*/
|
||||||
|
export const getTenantIDFromAppID = (appId: string) => {
|
||||||
|
if (!appId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const split = appId.split(SEPARATOR)
|
||||||
|
const hasDev = split[1] === DocumentTypes.DEV
|
||||||
|
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (hasDev) {
|
||||||
|
return split[2]
|
||||||
|
} else {
|
||||||
|
return split[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for automations, API endpoints should always be in context already
|
||||||
|
export const doInTenant = (tenantId: string | null, task: any) => {
|
||||||
|
// the internal function is so that we can re-use an existing
|
||||||
|
// context - don't want to close DB on a parent context
|
||||||
|
async function internal(opts = { existing: false }) {
|
||||||
|
// set the tenant id + global db if this is a new context
|
||||||
|
if (!opts.existing) {
|
||||||
|
updateTenantId(tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// invoke the task
|
||||||
|
return await task()
|
||||||
|
} finally {
|
||||||
|
await closeWithUsing(ContextKeys.TENANCY_IN_USE, () => {
|
||||||
|
return closeTenancy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
|
||||||
|
return updateUsing(ContextKeys.TENANCY_IN_USE, existing, internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doInAppContext = (appId: string, task: any) => {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("appId is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = getIdentity()
|
||||||
|
|
||||||
|
// the internal function is so that we can re-use an existing
|
||||||
|
// context - don't want to close DB on a parent context
|
||||||
|
async function internal(opts = { existing: false }) {
|
||||||
|
// set the app tenant id
|
||||||
|
if (!opts.existing) {
|
||||||
|
setAppTenantId(appId)
|
||||||
|
}
|
||||||
|
// set the app ID
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
|
||||||
|
// preserve the identity
|
||||||
|
if (identity) {
|
||||||
|
setIdentity(identity)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// invoke the task
|
||||||
|
return await task()
|
||||||
|
} finally {
|
||||||
|
await closeWithUsing(ContextKeys.APP_IN_USE, async () => {
|
||||||
|
await closeAppDBs()
|
||||||
|
await closeTenancy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existing = cls.getFromContext(ContextKeys.APP_ID) === appId
|
||||||
|
return updateUsing(ContextKeys.APP_IN_USE, existing, internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doInIdentityContext = (identity: IdentityContext, task: any) => {
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error("identity is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function internal(opts = { existing: false }) {
|
||||||
|
if (!opts.existing) {
|
||||||
|
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||||
|
// set the tenant so that doInTenant will preserve identity
|
||||||
|
if (identity.tenantId) {
|
||||||
|
updateTenantId(identity.tenantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// invoke the task
|
||||||
|
return await task()
|
||||||
|
} finally {
|
||||||
|
await closeWithUsing(ContextKeys.IDENTITY_IN_USE, async () => {
|
||||||
|
setIdentity(null)
|
||||||
|
await closeTenancy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = cls.getFromContext(ContextKeys.IDENTITY)
|
||||||
|
return updateUsing(ContextKeys.IDENTITY_IN_USE, existing, internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIdentity = (): IdentityContext | undefined => {
|
||||||
|
try {
|
||||||
|
return cls.getFromContext(ContextKeys.IDENTITY)
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing - identity is not in context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateTenantId = (tenantId: string | null) => {
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
if (env.USE_COUCH) {
|
||||||
|
setGlobalDB(tenantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAppId = async (appId: string) => {
|
||||||
|
try {
|
||||||
|
// have to close first, before removing the databases from context
|
||||||
|
await closeAppDBs()
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
} catch (err) {
|
||||||
|
if (env.isTest()) {
|
||||||
|
TEST_APP_ID = appId
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setGlobalDB = (tenantId: string | null) => {
|
||||||
|
const dbName = baseGlobalDBName(tenantId)
|
||||||
|
const db = dangerousGetDB(dbName)
|
||||||
|
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGlobalDB = () => {
|
||||||
|
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Global DB not found")
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isTenantIdSet = () => {
|
||||||
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||||
|
return !!tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTenantId = () => {
|
||||||
|
if (!isMultiTenant()) {
|
||||||
|
return DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||||
|
if (!tenantId) {
|
||||||
|
throw new Error("Tenant id not found")
|
||||||
|
}
|
||||||
|
return tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAppId = () => {
|
||||||
|
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
||||||
|
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||||
|
return TEST_APP_ID
|
||||||
|
} else {
|
||||||
|
return foundId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the app database based on whatever the request
|
||||||
|
* contained, dev or prod.
|
||||||
|
*/
|
||||||
|
export const getAppDB = (opts?: any) => {
|
||||||
|
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This specifically gets the prod app ID, if the request
|
||||||
|
* contained a development app ID, this will open the prod one.
|
||||||
|
*/
|
||||||
|
export const getProdAppDB = (opts?: any) => {
|
||||||
|
return getContextDB(ContextKeys.PROD_DB, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This specifically gets the dev app ID, if the request
|
||||||
|
* contained a prod app ID, this will open the dev one.
|
||||||
|
*/
|
||||||
|
export const getDevAppDB = (opts?: any) => {
|
||||||
|
return getContextDB(ContextKeys.DEV_DB, opts)
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
import "../../../tests/utilities/TestConfiguration"
|
||||||
|
import * as context from ".."
|
||||||
|
import { DEFAULT_TENANT_ID } from "../../constants"
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
|
// must use require to spy index file exports due to known issue in jest
|
||||||
|
const dbUtils = require("../../db")
|
||||||
|
jest.spyOn(dbUtils, "closeDB")
|
||||||
|
jest.spyOn(dbUtils, "dangerousGetDB")
|
||||||
|
|
||||||
|
describe("context", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("doInTenant", () => {
|
||||||
|
describe("single-tenancy", () => {
|
||||||
|
it("defaults to the default tenant", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe(DEFAULT_TENANT_ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("defaults to the default tenant db", async () => {
|
||||||
|
await context.doInTenant(DEFAULT_TENANT_ID, () => {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
expect(db.name).toBe("global-db")
|
||||||
|
})
|
||||||
|
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||||
|
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("multi-tenancy", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
env._set("MULTI_TENANCY", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails when no tenant id is set", () => {
|
||||||
|
const test = () => {
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
context.getTenantId()
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
expect(error.message).toBe("Tenant id not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test under no tenancy
|
||||||
|
test()
|
||||||
|
|
||||||
|
// test after tenancy has been accessed to ensure cleanup
|
||||||
|
context.doInTenant("test", () => {})
|
||||||
|
test()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails when no tenant db is set", () => {
|
||||||
|
const test = () => {
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
context.getGlobalDB()
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
expect(error.message).toBe("Global DB not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test under no tenancy
|
||||||
|
test()
|
||||||
|
|
||||||
|
// test after tenancy has been accessed to ensure cleanup
|
||||||
|
context.doInTenant("test", () => {})
|
||||||
|
test()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets tenant id", () => {
|
||||||
|
context.doInTenant("test", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("initialises the tenant db", async () => {
|
||||||
|
await context.doInTenant("test", () => {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
expect(db.name).toBe("test_global-db")
|
||||||
|
})
|
||||||
|
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||||
|
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the tenant id when nested with same tenant id", async () => {
|
||||||
|
await context.doInTenant("test", async () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("test")
|
||||||
|
|
||||||
|
await context.doInTenant("test", async () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("test")
|
||||||
|
|
||||||
|
await context.doInTenant("test", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("initialises the tenant db when nested with same tenant id", async () => {
|
||||||
|
await context.doInTenant("test", async () => {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
expect(db.name).toBe("test_global-db")
|
||||||
|
|
||||||
|
await context.doInTenant("test", async () => {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
expect(db.name).toBe("test_global-db")
|
||||||
|
|
||||||
|
await context.doInTenant("test", () => {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
expect(db.name).toBe("test_global-db")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// only 1 db is opened and closed
|
||||||
|
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||||
|
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets different tenant id inside another context", () => {
|
||||||
|
context.doInTenant("test", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("test")
|
||||||
|
|
||||||
|
context.doInTenant("nested", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("nested")
|
||||||
|
|
||||||
|
context.doInTenant("double-nested", () => {
|
||||||
|
const tenantId = context.getTenantId()
|
||||||
|
expect(tenantId).toBe("double-nested")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {
|
||||||
|
DEFAULT_TENANT_ID,
|
||||||
|
getAppId,
|
||||||
|
getTenantIDFromAppID,
|
||||||
|
updateTenantId,
|
||||||
|
} from "./index"
|
||||||
|
import cls from "./FunctionContext"
|
||||||
|
import { IdentityContext } from "@budibase/types"
|
||||||
|
import { ContextKeys } from "./constants"
|
||||||
|
import { dangerousGetDB, closeDB } from "../db"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
|
||||||
|
import env from "../environment"
|
||||||
|
|
||||||
|
export async function updateUsing(
|
||||||
|
usingKey: string,
|
||||||
|
existing: boolean,
|
||||||
|
internal: (opts: { existing: boolean }) => Promise<any>
|
||||||
|
) {
|
||||||
|
const using = cls.getFromContext(usingKey)
|
||||||
|
if (using && existing) {
|
||||||
|
cls.setOnContext(usingKey, using + 1)
|
||||||
|
return internal({ existing: true })
|
||||||
|
} else {
|
||||||
|
return cls.run(async () => {
|
||||||
|
cls.setOnContext(usingKey, 1)
|
||||||
|
return internal({ existing: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeWithUsing(
|
||||||
|
usingKey: string,
|
||||||
|
closeFn: () => Promise<any>
|
||||||
|
) {
|
||||||
|
const using = cls.getFromContext(usingKey)
|
||||||
|
if (!using || using <= 1) {
|
||||||
|
await closeFn()
|
||||||
|
} else {
|
||||||
|
cls.setOnContext(usingKey, using - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setAppTenantId = (appId: string) => {
|
||||||
|
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||||
|
updateTenantId(appTenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setIdentity = (identity: IdentityContext | null) => {
|
||||||
|
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function makes sure the PouchDB objects are closed and
|
||||||
|
// fully deleted when finished - this protects against memory leaks
|
||||||
|
export async function closeAppDBs() {
|
||||||
|
const dbKeys = [
|
||||||
|
ContextKeys.CURRENT_DB,
|
||||||
|
ContextKeys.PROD_DB,
|
||||||
|
ContextKeys.DEV_DB,
|
||||||
|
]
|
||||||
|
for (let dbKey of dbKeys) {
|
||||||
|
const db = cls.getFromContext(dbKey)
|
||||||
|
if (!db) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await closeDB(db)
|
||||||
|
// clear the DB from context, incase someone tries to use it again
|
||||||
|
cls.setOnContext(dbKey, null)
|
||||||
|
}
|
||||||
|
// clear the app ID now that the databases are closed
|
||||||
|
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, null)
|
||||||
|
}
|
||||||
|
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
||||||
|
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContextDB(key: string, opts: any) {
|
||||||
|
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
||||||
|
let storedOpts = cls.getFromContext(dbOptsKey)
|
||||||
|
let db = cls.getFromContext(key)
|
||||||
|
if (db && isEqual(opts, storedOpts)) {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = getAppId()
|
||||||
|
let toUseAppId
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case ContextKeys.CURRENT_DB:
|
||||||
|
toUseAppId = appId
|
||||||
|
break
|
||||||
|
case ContextKeys.PROD_DB:
|
||||||
|
toUseAppId = getProdAppID(appId)
|
||||||
|
break
|
||||||
|
case ContextKeys.DEV_DB:
|
||||||
|
toUseAppId = getDevelopmentAppID(appId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
db = dangerousGetDB(toUseAppId, opts)
|
||||||
|
try {
|
||||||
|
cls.setOnContext(key, db)
|
||||||
|
if (opts) {
|
||||||
|
cls.setOnContext(dbOptsKey, opts)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!env.isTest()) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ export enum AutomationViewModes {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ViewNames {
|
export enum ViewNames {
|
||||||
USER_BY_EMAIL = "by_email",
|
USER_BY_EMAIL = "by_email2",
|
||||||
BY_API_KEY = "by_api_key",
|
BY_API_KEY = "by_api_key",
|
||||||
USER_BY_BUILDERS = "by_builders",
|
USER_BY_BUILDERS = "by_builders",
|
||||||
LINK = "by_link",
|
LINK = "by_link",
|
||||||
|
@ -19,6 +19,13 @@ export enum ViewNames {
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DeprecatedViews = {
|
||||||
|
[ViewNames.USER_BY_EMAIL]: [
|
||||||
|
// removed due to inaccuracy in view doc filter logic
|
||||||
|
"by_email",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
export enum DocumentTypes {
|
export enum DocumentTypes {
|
||||||
USER = "us",
|
USER = "us",
|
||||||
WORKSPACE = "workspace",
|
WORKSPACE = "workspace",
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
const pouch = require("./pouch")
|
const pouch = require("./pouch")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const openDbs = []
|
||||||
let PouchDB
|
let PouchDB
|
||||||
let initialised = false
|
let initialised = false
|
||||||
const dbList = new Set()
|
const dbList = new Set()
|
||||||
|
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
setInterval(() => {
|
||||||
|
console.log("--- OPEN DBS ---")
|
||||||
|
console.log(openDbs)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
const put =
|
const put =
|
||||||
dbPut =>
|
dbPut =>
|
||||||
async (doc, options = {}) => {
|
async (doc, options = {}) => {
|
||||||
|
@ -35,6 +43,9 @@ exports.dangerousGetDB = (dbName, opts) => {
|
||||||
dbList.add(dbName)
|
dbList.add(dbName)
|
||||||
}
|
}
|
||||||
const db = new PouchDB(dbName, opts)
|
const db = new PouchDB(dbName, opts)
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
openDbs.push(db.name)
|
||||||
|
}
|
||||||
const dbPut = db.put
|
const dbPut = db.put
|
||||||
db.put = put(dbPut)
|
db.put = put(dbPut)
|
||||||
return db
|
return db
|
||||||
|
@ -46,6 +57,9 @@ exports.closeDB = async db => {
|
||||||
if (!db || env.isTest()) {
|
if (!db || env.isTest()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
openDbs.splice(openDbs.indexOf(db.name), 1)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// specifically await so that if there is an error, it can be ignored
|
// specifically await so that if there is an error, it can be ignored
|
||||||
return await db.close()
|
return await db.close()
|
||||||
|
|
|
@ -1,20 +1,42 @@
|
||||||
const { DocumentTypes, ViewNames } = require("./utils")
|
const {
|
||||||
|
DocumentTypes,
|
||||||
|
ViewNames,
|
||||||
|
DeprecatedViews,
|
||||||
|
SEPARATOR,
|
||||||
|
} = require("./utils")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
|
||||||
|
const DESIGN_DB = "_design/database"
|
||||||
|
|
||||||
function DesignDoc() {
|
function DesignDoc() {
|
||||||
return {
|
return {
|
||||||
_id: "_design/database",
|
_id: DESIGN_DB,
|
||||||
// view collation information, read before writing any complex views:
|
// view collation information, read before writing any complex views:
|
||||||
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
|
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
|
||||||
views: {},
|
views: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUserEmailView = async () => {
|
async function removeDeprecated(db, viewName) {
|
||||||
|
if (!DeprecatedViews[viewName]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const designDoc = await db.get(DESIGN_DB)
|
||||||
|
for (let deprecatedNames of DeprecatedViews[viewName]) {
|
||||||
|
delete designDoc.views[deprecatedNames]
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
} catch (err) {
|
||||||
|
// doesn't exist, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createNewUserEmailView = async () => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get(DESIGN_DB)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// no design doc, make one
|
// no design doc, make one
|
||||||
designDoc = DesignDoc()
|
designDoc = DesignDoc()
|
||||||
|
@ -22,7 +44,7 @@ exports.createUserEmailView = async () => {
|
||||||
const view = {
|
const view = {
|
||||||
// if using variables in a map function need to inject them before use
|
// if using variables in a map function need to inject them before use
|
||||||
map: `function(doc) {
|
map: `function(doc) {
|
||||||
if (doc._id.startsWith("${DocumentTypes.USER}")) {
|
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}")) {
|
||||||
emit(doc.email.toLowerCase(), doc._id)
|
emit(doc.email.toLowerCase(), doc._id)
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
|
@ -81,7 +103,7 @@ exports.createUserBuildersView = async () => {
|
||||||
|
|
||||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
const CreateFuncByName = {
|
const CreateFuncByName = {
|
||||||
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
}
|
}
|
||||||
|
@ -98,6 +120,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err != null && err.name === "not_found") {
|
if (err != null && err.name === "not_found") {
|
||||||
const createFunc = CreateFuncByName[viewName]
|
const createFunc = CreateFuncByName[viewName]
|
||||||
|
await removeDeprecated(db, viewName)
|
||||||
await createFunc()
|
await createFunc()
|
||||||
return exports.queryGlobalView(viewName, params)
|
return exports.queryGlobalView(viewName, params)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,7 +40,7 @@ const env = {
|
||||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
PLATFORM_URL: process.env.PLATFORM_URL,
|
PLATFORM_URL: process.env.PLATFORM_URL || "",
|
||||||
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
||||||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
||||||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||||
|
@ -54,6 +54,7 @@ const env = {
|
||||||
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
SERVICE: process.env.SERVICE || "budibase",
|
SERVICE: process.env.SERVICE || "budibase",
|
||||||
|
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
||||||
DEPLOYMENT_ENVIRONMENT:
|
DEPLOYMENT_ENVIRONMENT:
|
||||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PostHog from "posthog-node"
|
||||||
import { Event, Identity, Group, BaseEvent } from "@budibase/types"
|
import { Event, Identity, Group, BaseEvent } from "@budibase/types"
|
||||||
import { EventProcessor } from "./types"
|
import { EventProcessor } from "./types"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import context from "../../context"
|
import * as context from "../../context"
|
||||||
const pkg = require("../../../package.json")
|
const pkg = require("../../../package.json")
|
||||||
|
|
||||||
export default class PosthogProcessor implements EventProcessor {
|
export default class PosthogProcessor implements EventProcessor {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
getGlobalDBName,
|
getGlobalDBName,
|
||||||
getTenantId,
|
getTenantId,
|
||||||
} from "../tenancy"
|
} from "../tenancy"
|
||||||
import context from "../context"
|
import * as context from "../context"
|
||||||
import { DEFINITIONS } from "."
|
import { DEFINITIONS } from "."
|
||||||
import {
|
import {
|
||||||
Migration,
|
Migration,
|
||||||
|
|
|
@ -764,6 +764,11 @@
|
||||||
"@types/koa-compose" "*"
|
"@types/koa-compose" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/lodash@4.14.180":
|
||||||
|
version "4.14.180"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
||||||
|
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
|
||||||
|
|
||||||
"@types/mime@^1":
|
"@types/mime@^1":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.219-alpha.13",
|
"version": "1.1.10-alpha.4",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.219-alpha.13",
|
"@budibase/string-templates": "^1.1.10-alpha.4",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
@ -66,11 +66,12 @@
|
||||||
"@spectrum-css/radio": "^3.0.2",
|
"@spectrum-css/radio": "^3.0.2",
|
||||||
"@spectrum-css/search": "^3.0.2",
|
"@spectrum-css/search": "^3.0.2",
|
||||||
"@spectrum-css/sidenav": "^3.0.2",
|
"@spectrum-css/sidenav": "^3.0.2",
|
||||||
|
"@spectrum-css/slider": "3.0.1",
|
||||||
"@spectrum-css/statuslight": "^3.0.2",
|
"@spectrum-css/statuslight": "^3.0.2",
|
||||||
"@spectrum-css/stepper": "^3.0.3",
|
"@spectrum-css/stepper": "^3.0.3",
|
||||||
"@spectrum-css/switch": "^1.0.2",
|
"@spectrum-css/switch": "^1.0.2",
|
||||||
"@spectrum-css/table": "^3.0.1",
|
"@spectrum-css/table": "^3.0.1",
|
||||||
"@spectrum-css/tabs": "^3.0.1",
|
"@spectrum-css/tabs": "^3.2.12",
|
||||||
"@spectrum-css/tags": "^3.0.2",
|
"@spectrum-css/tags": "^3.0.2",
|
||||||
"@spectrum-css/textfield": "^3.0.1",
|
"@spectrum-css/textfield": "^3.0.1",
|
||||||
"@spectrum-css/toast": "^3.0.1",
|
"@spectrum-css/toast": "^3.0.1",
|
||||||
|
|
|
@ -82,6 +82,12 @@
|
||||||
.active svg {
|
.active svg {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
:global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.is-selected:not(.spectrum-ActionButton--emphasized) {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
.noPadding {
|
.noPadding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let size = "S"
|
export let size = "S"
|
||||||
export let extraButtonText
|
export let extraButtonText
|
||||||
export let extraButtonAction
|
export let extraButtonAction
|
||||||
|
export let showCloseButton = true
|
||||||
|
|
||||||
let show = true
|
let show = true
|
||||||
|
|
||||||
|
@ -39,22 +40,24 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="spectrum-Toast-buttons">
|
{#if showCloseButton}
|
||||||
<button
|
<div class="spectrum-Toast-buttons">
|
||||||
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}"
|
<button
|
||||||
on:click={clear}
|
class="spectrum-ClearButton spectrum-ClearButton--overBackground spectrum-ClearButton--size{size}"
|
||||||
>
|
on:click={clear}
|
||||||
<div class="spectrum-ClearButton-fill">
|
>
|
||||||
<svg
|
<div class="spectrum-ClearButton-fill">
|
||||||
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
|
<svg
|
||||||
focusable="false"
|
class="spectrum-ClearButton-icon spectrum-Icon spectrum-UIIcon-Cross100"
|
||||||
aria-hidden="true"
|
focusable="false"
|
||||||
>
|
aria-hidden="true"
|
||||||
<use xlink:href="#spectrum-css-icon-Cross100" />
|
>
|
||||||
</svg>
|
<use xlink:href="#spectrum-css-icon-Cross100" />
|
||||||
</div>
|
</svg>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -63,4 +66,7 @@
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.spectrum-Button {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
hr {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
hr.noMargin {
|
hr.noMargin {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let fillWidth
|
||||||
|
export let left = "314px"
|
||||||
|
export let width = "calc(100% - 576px)"
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
|
||||||
|
@ -42,7 +44,12 @@
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<Portal>
|
<Portal>
|
||||||
<section class:fillWidth class="drawer" transition:slide|local>
|
<section
|
||||||
|
class:fillWidth
|
||||||
|
class="drawer"
|
||||||
|
transition:slide|local
|
||||||
|
style={`width: ${width}; left: ${left};`}
|
||||||
|
>
|
||||||
<header>
|
<header>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Heading size="XS">{title}</Heading>
|
<Heading size="XS">{title}</Heading>
|
||||||
|
@ -69,8 +76,6 @@
|
||||||
.drawer {
|
.drawer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 260px;
|
|
||||||
width: calc(100% - 520px);
|
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-top: var(--border-light);
|
border-top: var(--border-light);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
|
@ -6,12 +6,15 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import clickOutside from "../../Actions/click_outside"
|
import clickOutside from "../../Actions/click_outside"
|
||||||
import Search from "./Search.svelte"
|
import Search from "./Search.svelte"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||||
|
|
||||||
export let id = null
|
export let id = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let fieldText = ""
|
export let fieldText = ""
|
||||||
export let fieldIcon = ""
|
export let fieldIcon = ""
|
||||||
|
export let fieldColour = ""
|
||||||
export let isPlaceholder = false
|
export let isPlaceholder = false
|
||||||
export let placeholderOption = null
|
export let placeholderOption = null
|
||||||
export let options = []
|
export let options = []
|
||||||
|
@ -20,6 +23,7 @@
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
|
export let getOptionColour = () => null
|
||||||
export let open = false
|
export let open = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
@ -83,11 +87,10 @@
|
||||||
on:mousedown={onClick}
|
on:mousedown={onClick}
|
||||||
>
|
>
|
||||||
{#if fieldIcon}
|
{#if fieldIcon}
|
||||||
<span class="icon-Placeholder-Padding">
|
<span class="option-icon">
|
||||||
<img src={fieldIcon} alt="icon" width="20" height="15" />
|
<Icon name={fieldIcon} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="spectrum-Picker-label"
|
class="spectrum-Picker-label"
|
||||||
class:is-placeholder={isPlaceholder}
|
class:is-placeholder={isPlaceholder}
|
||||||
|
@ -105,6 +108,11 @@
|
||||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if fieldColour}
|
||||||
|
<span class="option-colour">
|
||||||
|
<StatusLight size="L" color={fieldColour} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -158,13 +166,8 @@
|
||||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||||
>
|
>
|
||||||
{#if getOptionIcon(option, idx)}
|
{#if getOptionIcon(option, idx)}
|
||||||
<span class="icon-Padding">
|
<span class="option-icon">
|
||||||
<img
|
<Icon name={getOptionIcon(option, idx)} />
|
||||||
src={getOptionIcon(option, idx)}
|
|
||||||
alt="icon"
|
|
||||||
width="20"
|
|
||||||
height="15"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
@ -177,6 +180,11 @@
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{#if getOptionColour(option, idx)}
|
||||||
|
<span class="option-colour">
|
||||||
|
<StatusLight size="L" color={getOptionColour(option, idx)} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -214,13 +222,18 @@
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Padding {
|
/* Icon and colour alignment */
|
||||||
padding-right: 10px;
|
.spectrum-Menu-checkmark {
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.icon-Placeholder-Padding {
|
.option-colour {
|
||||||
padding-top: 5px;
|
padding-left: 8px;
|
||||||
padding-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
.option-icon {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Popover :global(.spectrum-Search) {
|
.spectrum-Popover :global(.spectrum-Search) {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
export let id = null
|
export let id = null
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
export let inputRef
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@
|
||||||
type="search"
|
type="search"
|
||||||
class="spectrum-Textfield-input spectrum-Search-input"
|
class="spectrum-Textfield-input spectrum-Search-input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
bind:this={inputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
|
export let getOptionColour = () => null
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
|
@ -20,7 +21,19 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let open = false
|
let open = false
|
||||||
$: fieldText = getFieldText(value, options, placeholder)
|
$: fieldText = getFieldText(value, options, placeholder)
|
||||||
$: fieldIcon = getFieldIcon(value, options, placeholder)
|
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||||
|
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||||
|
|
||||||
|
const getFieldAttribute = (getAttribute, value, options) => {
|
||||||
|
// Wait for options to load if there is a value but no options
|
||||||
|
if (!options?.length) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const index = options.findIndex(
|
||||||
|
(option, idx) => getOptionValue(option, idx) === value
|
||||||
|
)
|
||||||
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
|
}
|
||||||
|
|
||||||
const getFieldText = (value, options, placeholder) => {
|
const getFieldText = (value, options, placeholder) => {
|
||||||
// Always use placeholder if no value
|
// Always use placeholder if no value
|
||||||
|
@ -28,27 +41,7 @@
|
||||||
return placeholder || "Choose an option"
|
return placeholder || "Choose an option"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for options to load if there is a value but no options
|
return getFieldAttribute(getOptionLabel, value, options)
|
||||||
if (!options?.length) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the label if the selected option is found, otherwise raw value
|
|
||||||
const index = options.findIndex(
|
|
||||||
(option, idx) => getOptionValue(option, idx) === value
|
|
||||||
)
|
|
||||||
return index !== -1 ? getOptionLabel(options[index], index) : value
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldIcon = (value, options) => {
|
|
||||||
// Wait for options to load if there is a value but no options
|
|
||||||
if (!options?.length) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const index = options.findIndex(
|
|
||||||
(option, idx) => getOptionValue(option, idx) === value
|
|
||||||
)
|
|
||||||
return index !== -1 ? getOptionIcon(options[index], index) : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectOption = value => {
|
const selectOption = value => {
|
||||||
|
@ -66,12 +59,14 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{fieldText}
|
{fieldText}
|
||||||
|
{fieldIcon}
|
||||||
|
{fieldColour}
|
||||||
{options}
|
{options}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
{fieldIcon}
|
{getOptionColour}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/slider/dist/index-vars.css"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = false
|
||||||
|
export let id = null
|
||||||
|
export let disabled = false
|
||||||
|
export let min = 0
|
||||||
|
export let max = 100
|
||||||
|
export let step = 1
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = event => {
|
||||||
|
dispatch("change", event.target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
{step}
|
||||||
|
{value}
|
||||||
|
{disabled}
|
||||||
|
{id}
|
||||||
|
on:change={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
input::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: 2px solid var(--spectrum-global-color-gray-700);
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--background);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
border: 2px solid var(--spectrum-global-color-gray-700);
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--background);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,3 +12,4 @@ export { default as CoreDatePicker } from "./DatePicker.svelte"
|
||||||
export { default as CoreDropzone } from "./Dropzone.svelte"
|
export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
|
export { default as CoreSlider } from "./Slider.svelte"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
export let inputRef
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{quiet}
|
{quiet}
|
||||||
|
bind:inputRef
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
export let getOptionIcon = option => option?.icon
|
export let getOptionIcon = option => option?.icon
|
||||||
|
export let getOptionColour = option => option?.colour
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
{getOptionIcon}
|
{getOptionIcon}
|
||||||
|
{getOptionColour}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import Slider from "./Core/Slider.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let min = 0
|
||||||
|
export let max = 100
|
||||||
|
export let step = 1
|
||||||
|
export let disabled = false
|
||||||
|
export let error = null
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} />
|
||||||
|
</Field>
|
|
@ -47,7 +47,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{#if tooltip && showTooltip}
|
{#if tooltip && showTooltip}
|
||||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||||
<Tooltip textWrapping direction={"bottom"} text={tooltip} />
|
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="icon-side-nav">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-side-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
|
export let icon
|
||||||
|
export let active = false
|
||||||
|
export let tooltip
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon-side-nav-item"
|
||||||
|
class:active
|
||||||
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:focus={() => (showTooltip = true)}
|
||||||
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:click
|
||||||
|
>
|
||||||
|
<Icon name={icon} hoverable />
|
||||||
|
{#if tooltip && showTooltip}
|
||||||
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||||
|
<Tooltip textWrapping direction="right" text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-side-nav-item {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.icon-side-nav-item:hover :global(svg),
|
||||||
|
.active :global(svg) {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: calc(100% - 4px);
|
||||||
|
top: 50%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,7 +9,7 @@
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div class="notifications">
|
<div class="notifications">
|
||||||
{#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
|
{#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
|
||||||
<div transition:fly={{ y: -30 }}>
|
<div transition:fly={{ y: 30 }}>
|
||||||
<Notification
|
<Notification
|
||||||
{type}
|
{type}
|
||||||
{icon}
|
{icon}
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<style>
|
<style>
|
||||||
.notifications {
|
.notifications {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
bottom: 40px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
export let negative = false
|
export let negative = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let active = false
|
export let active = false
|
||||||
|
export let color = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
||||||
|
class:custom={!!color}
|
||||||
|
style={`--color: ${color};`}
|
||||||
class:spectrum-StatusLight--celery={celery}
|
class:spectrum-StatusLight--celery={celery}
|
||||||
class:spectrum-StatusLight--yellow={yellow}
|
class:spectrum-StatusLight--yellow={yellow}
|
||||||
class:spectrum-StatusLight--fuchsia={fuchsia}
|
class:spectrum-StatusLight--fuchsia={fuchsia}
|
||||||
|
@ -36,6 +39,26 @@
|
||||||
class:spectrum-StatusLight--negative={negative}
|
class:spectrum-StatusLight--negative={negative}
|
||||||
class:spectrum-StatusLight--disabled={disabled}
|
class:spectrum-StatusLight--disabled={disabled}
|
||||||
class:spectrum-StatusLight--active={active}
|
class:spectrum-StatusLight--active={active}
|
||||||
|
class:withText={!!$$slots.default}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-StatusLight {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
--spectrum-statuslight-info-text-gap: 4px;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.spectrum-StatusLight.withText::before {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.custom::before {
|
||||||
|
background: var(--color) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -79,4 +79,10 @@
|
||||||
.emphasized {
|
.emphasized {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
.spectrum-Tabs-item {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.spectrum-Tabs-item.is-selected {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
export let noHorizPadding = false
|
export let noHorizPadding = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let emphasized = false
|
export let emphasized = false
|
||||||
// overlay content from the tab bar onto tabs e.g. for a dropdown
|
export let size = "M"
|
||||||
export let onTop = false
|
|
||||||
|
|
||||||
let thisSelected = undefined
|
let thisSelected = undefined
|
||||||
|
|
||||||
|
@ -74,20 +73,18 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class:quiet
|
class:spectrum-Tabs--quiet={quiet}
|
||||||
class:noHorizPadding
|
class:noHorizPadding
|
||||||
class="selected-border spectrum-Tabs {quiet &&
|
class:spectrum-Tabs--vertical={vertical}
|
||||||
'spectrum-Tabs--quiet'} spectrum-Tabs--{vertical
|
class:spectrum-Tabs--horizontal={!vertical}
|
||||||
? 'vertical'
|
class="spectrum-Tabs spectrum-Tabs--size{size}"
|
||||||
: 'horizontal'}"
|
|
||||||
class:onTop
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
{#if $tab.info}
|
{#if $tab.info}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Tabs-selectionIndicator indicator-transition"
|
class="spectrum-Tabs-selectionIndicator"
|
||||||
style="{emphasized &&
|
class:emphasized
|
||||||
'background-color: var(--spectrum-global-color-blue-400)'}; width: {width}; height: {height}; left: {left}; top: {top};"
|
style="width: {width}; height: {height}; left: {left}; top: {top};"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,26 +95,26 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.quiet {
|
.spectrum-Tabs--quiet {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
.onTop {
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
.spectrum-Tabs {
|
.spectrum-Tabs {
|
||||||
padding-left: var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
border-bottom-color: var(--spectrum-global-color-gray-200);
|
||||||
}
|
}
|
||||||
.spectrum-Tabs-content {
|
.spectrum-Tabs-content {
|
||||||
margin-top: var(--spectrum-global-dimension-static-size-150);
|
margin-top: var(--spectrum-global-dimension-static-size-150);
|
||||||
}
|
}
|
||||||
.indicator-transition {
|
.spectrum-Tabs-selectionIndicator {
|
||||||
transition: all 200ms;
|
transition: all 200ms;
|
||||||
|
background-color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
|
.spectrum-Tabs-selectionIndicator.emphasized {
|
||||||
|
background-color: var(--spectrum-global-color-blue-400);
|
||||||
}
|
}
|
||||||
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
||||||
bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
.noHorizPadding {
|
.noHorizPadding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
|
<!-- Showing / Hiding a text wrapped tooltip should be handled outside the component -->
|
||||||
{#if textWrapping}
|
{#if textWrapping}
|
||||||
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open">
|
<span class="spectrum-Tooltip spectrum-Tooltip--{direction} is-open tooltip">
|
||||||
<span class="spectrum-Tooltip-label">{text}</span>
|
<span class="spectrum-Tooltip-label">{text}</span>
|
||||||
<span class="spectrum-Tooltip-tip" />
|
<span class="spectrum-Tooltip-tip" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -22,3 +22,9 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tooltip {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -64,6 +64,9 @@ export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
|
export { default as Slider } from "./Form/Slider.svelte"
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
|
|
|
@ -206,6 +206,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713"
|
||||||
integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g==
|
integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g==
|
||||||
|
|
||||||
|
"@spectrum-css/slider@3.0.1":
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@spectrum-css/slider/-/slider-3.0.1.tgz#5281e6f47eb5a4fd3d1816c138bf66d01d7f2e49"
|
||||||
|
integrity sha512-DI2dtMRnQuDM1miVzl3SGyR1khUEKnwdXfO5EHDFwkC3yav43F5QogkfjmjFmWWobMVovdJlAuiaaJ/IHejD0Q==
|
||||||
|
|
||||||
"@spectrum-css/statuslight@^3.0.2":
|
"@spectrum-css/statuslight@^3.0.2":
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5"
|
||||||
|
@ -226,10 +231,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/table/-/table-3.0.2.tgz#c666743d569fef81ddc8810fac8cda53b315f8d7"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/table/-/table-3.0.2.tgz#c666743d569fef81ddc8810fac8cda53b315f8d7"
|
||||||
integrity sha512-nt/QNC7NmUank0wozd4FySEX1UIYXuvuOKDyN1II3sxfwFSpJfp/Df9KVMhrYs4EsmB4XMGcoxp8ND/CrvH3ow==
|
integrity sha512-nt/QNC7NmUank0wozd4FySEX1UIYXuvuOKDyN1II3sxfwFSpJfp/Df9KVMhrYs4EsmB4XMGcoxp8ND/CrvH3ow==
|
||||||
|
|
||||||
"@spectrum-css/tabs@^3.0.1":
|
"@spectrum-css/tabs@^3.2.12":
|
||||||
version "3.0.2"
|
version "3.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.0.2.tgz#822316672e7b0dfba66faa988e638ddae18c700e"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.2.12.tgz#9b08f23d5aa881b3441af7757800c7173e5685ff"
|
||||||
integrity sha512-4RNcmwf0wxLpB7M54H02owlj0mKE8neL1+lytQpxOOhlwTO5zdsD82zjvx9tIc8tRnRKuhCCCwTuBxHYstnBmw==
|
integrity sha512-rPFUW9SSW4+3/UJ3UrtY2/l3sQvlqB1fqxHLPDjgykvbfrnMejcCTNV4ZrFNHXpE/6+kGnk+yVViSPtWGwJzkA==
|
||||||
|
|
||||||
"@spectrum-css/tags@^3.0.2":
|
"@spectrum-css/tags@^3.0.2":
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.219-alpha.13",
|
"version": "1.1.10-alpha.4",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.219-alpha.13",
|
"@budibase/bbui": "^1.1.10-alpha.4",
|
||||||
"@budibase/client": "^1.0.219-alpha.13",
|
"@budibase/client": "^1.1.10-alpha.4",
|
||||||
"@budibase/frontend-core": "^1.0.219-alpha.13",
|
"@budibase/frontend-core": "^1.1.10-alpha.4",
|
||||||
"@budibase/string-templates": "^1.0.219-alpha.13",
|
"@budibase/string-templates": "^1.1.10-alpha.4",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.13.12",
|
||||||
"@babel/runtime": "^7.13.10",
|
"@babel/runtime": "^7.13.10",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@roxi/routify": "2.18.0",
|
"@roxi/routify": "2.18.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19",
|
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/svelte": "^3.0.0",
|
"@testing-library/svelte": "^3.0.0",
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"start-server-and-test": "^1.12.1",
|
"start-server-and-test": "^1.12.1",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.49.0",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import { JSONUtils } from "@budibase/frontend-core"
|
import { JSONUtils } from "@budibase/frontend-core"
|
||||||
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
|
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
@ -478,11 +478,17 @@ const getUrlBindings = asset => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const safeURL = makePropSafe("url")
|
const safeURL = makePropSafe("url")
|
||||||
return params.map(param => ({
|
const urlParamBindings = params.map(param => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
|
runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
|
||||||
readableBinding: `URL.${param}`,
|
readableBinding: `URL.${param}`,
|
||||||
}))
|
}))
|
||||||
|
const queryParamsBinding = {
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: makePropSafe("query"),
|
||||||
|
readableBinding: "Query params",
|
||||||
|
}
|
||||||
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoleBindings = () => {
|
const getRoleBindings = () => {
|
||||||
|
@ -782,6 +788,13 @@ export const getAllStateVariables = () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add on load settings from screens
|
||||||
|
get(store).screens.forEach(screen => {
|
||||||
|
if (screen.onLoad) {
|
||||||
|
eventSettings.push(screen.onLoad)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Extract all state keys from any "update state" actions in each setting
|
// Extract all state keys from any "update state" actions in each setting
|
||||||
let bindingSet = new Set()
|
let bindingSet = new Set()
|
||||||
eventSettings.forEach(setting => {
|
eventSettings.forEach(setting => {
|
||||||
|
|
|
@ -1,65 +1,77 @@
|
||||||
import { getFrontendStore } from "./store/frontend"
|
import { getFrontendStore } from "./store/frontend"
|
||||||
import { getAutomationStore } from "./store/automation"
|
import { getAutomationStore } from "./store/automation"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
import { LAYOUT_NAMES } from "../constants"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
|
|
||||||
export const currentAsset = derived(store, $store => {
|
export const selectedScreen = derived(store, $store => {
|
||||||
const type = $store.currentFrontEndType
|
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||||
if (type === FrontendTypes.SCREEN) {
|
})
|
||||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
|
||||||
} else if (type === FrontendTypes.LAYOUT) {
|
export const selectedLayout = derived(store, $store => {
|
||||||
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId)
|
return $store.layouts?.find(layout => layout._id === $store.selectedLayoutId)
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectedComponent = derived(
|
export const selectedComponent = derived(
|
||||||
[store, currentAsset],
|
[store, selectedScreen],
|
||||||
([$store, $currentAsset]) => {
|
([$store, $selectedScreen]) => {
|
||||||
if (!$currentAsset || !$store.selectedComponentId) {
|
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
return findComponent($selectedScreen?.props, $store.selectedComponentId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const sortedScreens = derived(store, $store => {
|
||||||
|
return $store.screens.slice().sort((a, b) => {
|
||||||
|
// Sort by role first
|
||||||
|
const roleA = RoleUtils.getRolePriority(a.routing.roleId)
|
||||||
|
const roleB = RoleUtils.getRolePriority(b.routing.roleId)
|
||||||
|
if (roleA !== roleB) {
|
||||||
|
return roleA > roleB ? -1 : 1
|
||||||
|
}
|
||||||
|
// Then put home screens first
|
||||||
|
const homeA = !!a.routing.homeScreen
|
||||||
|
const homeB = !!b.routing.homeScreen
|
||||||
|
if (homeA !== homeB) {
|
||||||
|
return homeA ? -1 : 1
|
||||||
|
}
|
||||||
|
// Then sort alphabetically by each URL param
|
||||||
|
const aParams = a.routing.route.split("/")
|
||||||
|
const bParams = b.routing.route.split("/")
|
||||||
|
let minParams = Math.min(aParams.length, bParams.length)
|
||||||
|
for (let i = 0; i < minParams; i++) {
|
||||||
|
if (aParams[i] === bParams[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return aParams[i] < bParams[i] ? -1 : 1
|
||||||
|
}
|
||||||
|
// Then sort by the fewest amount of URL params
|
||||||
|
return aParams.length < bParams.length ? -1 : 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export const selectedComponentPath = derived(
|
export const selectedComponentPath = derived(
|
||||||
[store, currentAsset],
|
[store, selectedScreen],
|
||||||
([$store, $currentAsset]) => {
|
([$store, $selectedScreen]) => {
|
||||||
return findComponentPath(
|
return findComponentPath(
|
||||||
$currentAsset?.props,
|
$selectedScreen?.props,
|
||||||
$store.selectedComponentId
|
$store.selectedComponentId
|
||||||
).map(component => component._id)
|
).map(component => component._id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const currentAssetId = derived(store, $store => {
|
|
||||||
return $store.currentFrontEndType === FrontendTypes.SCREEN
|
|
||||||
? $store.selectedScreenId
|
|
||||||
: $store.selectedLayoutId
|
|
||||||
})
|
|
||||||
|
|
||||||
export const currentAssetName = derived(currentAsset, $currentAsset => {
|
|
||||||
return $currentAsset?.name
|
|
||||||
})
|
|
||||||
|
|
||||||
// leave this as before for consistency
|
|
||||||
export const allScreens = derived(store, $store => {
|
|
||||||
return $store.screens
|
|
||||||
})
|
|
||||||
|
|
||||||
export const mainLayout = derived(store, $store => {
|
export const mainLayout = derived(store, $store => {
|
||||||
return $store.layouts?.find(
|
return $store.layouts?.find(
|
||||||
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
|
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectedAccessRole = writable("BASIC")
|
// For compatibility
|
||||||
|
export const currentAsset = selectedScreen
|
||||||
export const screenSearchString = writable(null)
|
|
||||||
|
|
|
@ -68,7 +68,19 @@ const automationActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
duplicate: async automation => {
|
||||||
|
const response = await API.createAutomation({
|
||||||
|
...automation,
|
||||||
|
name: `${automation.name} - copy`,
|
||||||
|
_id: undefined,
|
||||||
|
_ref: undefined,
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.automations = [...state.automations, response.automation]
|
||||||
|
store.actions.select(response.automation)
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
save: async automation => {
|
save: async automation => {
|
||||||
const response = await API.updateAutomation(automation)
|
const response = await API.updateAutomation(automation)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { get, writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import { currentAsset, mainLayout, selectedComponent } from "builderStore"
|
||||||
allScreens,
|
|
||||||
currentAsset,
|
|
||||||
mainLayout,
|
|
||||||
selectedComponent,
|
|
||||||
selectedAccessRole,
|
|
||||||
} from "builderStore"
|
|
||||||
import {
|
import {
|
||||||
datasources,
|
datasources,
|
||||||
integrations,
|
integrations,
|
||||||
|
@ -15,7 +9,6 @@ import {
|
||||||
tables,
|
tables,
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { FrontendTypes } from "constants"
|
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import {
|
import {
|
||||||
findComponentType,
|
findComponentType,
|
||||||
|
@ -27,6 +20,7 @@ import {
|
||||||
makeComponentUnique,
|
makeComponentUnique,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -47,10 +41,6 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
messagePassing: false,
|
messagePassing: false,
|
||||||
continueIfAction: false,
|
continueIfAction: false,
|
||||||
},
|
},
|
||||||
currentFrontEndType: "none",
|
|
||||||
selectedScreenId: "",
|
|
||||||
selectedLayoutId: "",
|
|
||||||
selectedComponentId: "",
|
|
||||||
errors: [],
|
errors: [],
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
libraries: null,
|
libraries: null,
|
||||||
|
@ -61,6 +51,11 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
customTheme: {},
|
customTheme: {},
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
highlightedSettingKey: null,
|
highlightedSettingKey: null,
|
||||||
|
|
||||||
|
// URL params
|
||||||
|
selectedScreenId: null,
|
||||||
|
selectedComponentId: null,
|
||||||
|
selectedLayoutId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
@ -100,6 +95,7 @@ export const getFrontendStore = () => {
|
||||||
previousTopNavPath: {},
|
previousTopNavPath: {},
|
||||||
version: application.version,
|
version: application.version,
|
||||||
revertableVersion: application.revertableVersion,
|
revertableVersion: application.revertableVersion,
|
||||||
|
navigation: application.navigation || {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Initialise backend stores
|
// Initialise backend stores
|
||||||
|
@ -108,6 +104,35 @@ export const getFrontendStore = () => {
|
||||||
await integrations.init()
|
await integrations.init()
|
||||||
await queries.init()
|
await queries.init()
|
||||||
await tables.init()
|
await tables.init()
|
||||||
|
|
||||||
|
// Add navigation settings to old apps
|
||||||
|
if (!application.navigation) {
|
||||||
|
const layout = layouts.find(x => x._id === LAYOUT_NAMES.MASTER.PRIVATE)
|
||||||
|
const customTheme = application.customTheme
|
||||||
|
let navigationSettings = {
|
||||||
|
navigation: "Top",
|
||||||
|
title: application.name,
|
||||||
|
navWidth: "Large",
|
||||||
|
navBackground:
|
||||||
|
customTheme?.navBackground || DefaultAppTheme.navBackground,
|
||||||
|
navTextColor:
|
||||||
|
customTheme?.navTextColor || DefaultAppTheme.navTextColor,
|
||||||
|
}
|
||||||
|
if (layout) {
|
||||||
|
navigationSettings.hideLogo = layout.props.hideLogo
|
||||||
|
navigationSettings.hideTitle = layout.props.hideTitle
|
||||||
|
navigationSettings.title = layout.props.title || application.name
|
||||||
|
navigationSettings.logoUrl = layout.props.logoUrl
|
||||||
|
navigationSettings.links = layout.props.links
|
||||||
|
navigationSettings.navigation = layout.props.navigation || "Top"
|
||||||
|
navigationSettings.sticky = layout.props.sticky
|
||||||
|
navigationSettings.navWidth = layout.props.width || "Large"
|
||||||
|
if (navigationSettings.navigation === "None") {
|
||||||
|
navigationSettings.navigation = "Top"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await store.actions.navigation.save(navigationSettings)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
save: async theme => {
|
save: async theme => {
|
||||||
|
@ -135,6 +160,19 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
navigation: {
|
||||||
|
save: async navigation => {
|
||||||
|
const appId = get(store).appId
|
||||||
|
await API.saveAppMetadata({
|
||||||
|
appId,
|
||||||
|
metadata: { navigation },
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.navigation = navigation
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
routing: {
|
routing: {
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const response = await API.fetchAppRoutes()
|
const response = await API.fetchAppRoutes()
|
||||||
|
@ -147,18 +185,12 @@ export const getFrontendStore = () => {
|
||||||
screens: {
|
screens: {
|
||||||
select: screenId => {
|
select: screenId => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
let screens = get(allScreens)
|
let screens = state.screens
|
||||||
let screen =
|
let screen =
|
||||||
screens.find(screen => screen._id === screenId) || screens[0]
|
screens.find(screen => screen._id === screenId) || screens[0]
|
||||||
if (!screen) return state
|
if (!screen) return state
|
||||||
|
|
||||||
// Update role to the screen's role setting so that it will always
|
|
||||||
// be visible
|
|
||||||
selectedAccessRole.set(screen.routing.roleId)
|
|
||||||
|
|
||||||
state.currentFrontEndType = FrontendTypes.SCREEN
|
|
||||||
state.selectedScreenId = screen._id
|
state.selectedScreenId = screen._id
|
||||||
state.currentView = "detail"
|
|
||||||
state.selectedComponentId = screen.props?._id
|
state.selectedComponentId = screen.props?._id
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
@ -221,16 +253,44 @@ export const getFrontendStore = () => {
|
||||||
// Refresh routes
|
// Refresh routes
|
||||||
await store.actions.routing.fetch()
|
await store.actions.routing.fetch()
|
||||||
},
|
},
|
||||||
|
updateHomeScreen: async (screen, makeHomeScreen = true) => {
|
||||||
|
let promises = []
|
||||||
|
|
||||||
|
// Find any existing home screen for this role so we can remove it,
|
||||||
|
// if we are setting this to be the new home screen
|
||||||
|
if (makeHomeScreen) {
|
||||||
|
const roleId = screen.routing.roleId
|
||||||
|
let existingHomeScreen = get(store).screens.find(s => {
|
||||||
|
return (
|
||||||
|
s.routing.roleId === roleId &&
|
||||||
|
s.routing.homeScreen &&
|
||||||
|
s._id !== screen._id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (existingHomeScreen) {
|
||||||
|
existingHomeScreen.routing.homeScreen = false
|
||||||
|
promises.push(store.actions.screens.save(existingHomeScreen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the passed in screen
|
||||||
|
screen.routing.homeScreen = makeHomeScreen
|
||||||
|
promises.push(store.actions.screens.save(screen))
|
||||||
|
return await Promise.all(promises)
|
||||||
|
},
|
||||||
|
removeCustomLayout: async screen => {
|
||||||
|
// Pull relevant settings from old layout, if required
|
||||||
|
const layout = get(store).layouts.find(x => x._id === screen.layoutId)
|
||||||
|
screen.layoutId = null
|
||||||
|
screen.showNavigation = layout?.props.navigation !== "None"
|
||||||
|
screen.width = layout?.props.width || "Large"
|
||||||
|
await store.actions.screens.save(screen)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
saveSelected: async () => {
|
saveSelected: async () => {
|
||||||
const state = get(store)
|
|
||||||
const selectedAsset = get(currentAsset)
|
const selectedAsset = get(currentAsset)
|
||||||
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
|
return await store.actions.screens.save(selectedAsset)
|
||||||
return await store.actions.screens.save(selectedAsset)
|
|
||||||
} else {
|
|
||||||
return await store.actions.layouts.save(selectedAsset)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setDevice: device => {
|
setDevice: device => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -245,8 +305,6 @@ export const getFrontendStore = () => {
|
||||||
const layout =
|
const layout =
|
||||||
store.actions.layouts.find(layoutId) || get(store).layouts[0]
|
store.actions.layouts.find(layoutId) || get(store).layouts[0]
|
||||||
if (!layout) return
|
if (!layout) return
|
||||||
state.currentFrontEndType = FrontendTypes.LAYOUT
|
|
||||||
state.currentView = "detail"
|
|
||||||
state.selectedLayoutId = layout._id
|
state.selectedLayoutId = layout._id
|
||||||
state.selectedComponentId = layout.props?._id
|
state.selectedComponentId = layout.props?._id
|
||||||
return state
|
return state
|
||||||
|
@ -297,32 +355,6 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
select: component => {
|
|
||||||
const asset = get(currentAsset)
|
|
||||||
if (!asset || !component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the root component, select the asset instead
|
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
|
||||||
if (parent == null) {
|
|
||||||
const state = get(store)
|
|
||||||
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
|
|
||||||
if (isLayout) {
|
|
||||||
store.actions.layouts.select(asset._id)
|
|
||||||
} else {
|
|
||||||
store.actions.screens.select(asset._id)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise select the component
|
|
||||||
store.update(state => {
|
|
||||||
state.selectedComponentId = component._id
|
|
||||||
state.currentView = "component"
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getDefinition: componentName => {
|
getDefinition: componentName => {
|
||||||
if (!componentName) {
|
if (!componentName) {
|
||||||
return null
|
return null
|
||||||
|
@ -418,7 +450,6 @@ export const getFrontendStore = () => {
|
||||||
// Save components and update UI
|
// Save components and update UI
|
||||||
await store.actions.preview.saveSelected()
|
await store.actions.preview.saveSelected()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.currentView = "component"
|
|
||||||
state.selectedComponentId = componentInstance._id
|
state.selectedComponentId = componentInstance._id
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
@ -461,11 +492,14 @@ export const getFrontendStore = () => {
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
store.actions.components.select(parent)
|
store.update(state => {
|
||||||
|
state.selectedComponentId = parent._id
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
await store.actions.preview.saveSelected()
|
await store.actions.preview.saveSelected()
|
||||||
},
|
},
|
||||||
copy: (component, cut = false) => {
|
copy: (component, cut = false, selectParent = true) => {
|
||||||
const selectedAsset = get(currentAsset)
|
const selectedAsset = get(currentAsset)
|
||||||
if (!selectedAsset) {
|
if (!selectedAsset) {
|
||||||
return null
|
return null
|
||||||
|
@ -485,7 +519,12 @@ export const getFrontendStore = () => {
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
store.actions.components.select(parent)
|
if (selectParent) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = parent._id
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -536,7 +575,7 @@ export const getFrontendStore = () => {
|
||||||
|
|
||||||
// Save and select the new component
|
// Save and select the new component
|
||||||
promises.push(store.actions.preview.saveSelected())
|
promises.push(store.actions.preview.saveSelected())
|
||||||
store.actions.components.select(componentToPaste)
|
state.selectedComponentId = componentToPaste._id
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
@ -578,35 +617,38 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
save: async (url, title) => {
|
save: async (url, title) => {
|
||||||
const layout = get(mainLayout)
|
const navigation = get(store).navigation
|
||||||
if (!layout) {
|
let links = [...navigation?.links]
|
||||||
|
|
||||||
|
// Skip if we have an identical link
|
||||||
|
if (links.find(link => link.url === url && link.text === title)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add link setting to main layout
|
links.push({
|
||||||
if (!layout.props.links) {
|
|
||||||
layout.props.links = []
|
|
||||||
}
|
|
||||||
layout.props.links.push({
|
|
||||||
text: title,
|
text: title,
|
||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
|
await store.actions.navigation.save({
|
||||||
await store.actions.layouts.save(layout)
|
...navigation,
|
||||||
|
links: [...links],
|
||||||
|
})
|
||||||
},
|
},
|
||||||
delete: async urls => {
|
delete: async urls => {
|
||||||
const layout = get(mainLayout)
|
const navigation = get(store).navigation
|
||||||
if (!layout?.props.links?.length) {
|
let links = navigation?.links
|
||||||
|
if (!links?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out the URLs to delete
|
// Filter out the URLs to delete
|
||||||
urls = Array.isArray(urls) ? urls : [urls]
|
urls = Array.isArray(urls) ? urls : [urls]
|
||||||
layout.props.links = layout.props.links.filter(
|
links = links.filter(link => !urls.includes(link.url))
|
||||||
link => !urls.includes(link.url)
|
|
||||||
)
|
|
||||||
|
|
||||||
await store.actions.layouts.save(layout)
|
await store.actions.navigation.save({
|
||||||
|
...navigation,
|
||||||
|
links,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
|
|
|
@ -5,7 +5,8 @@ export class Screen extends BaseStructure {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(true)
|
super(true)
|
||||||
this._json = {
|
this._json = {
|
||||||
layoutId: "layout_private_master",
|
showNavigation: true,
|
||||||
|
width: "Large",
|
||||||
props: {
|
props: {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: "@budibase/standard-components/container",
|
_component: "@budibase/standard-components/container",
|
||||||
|
@ -26,6 +27,7 @@ export class Screen extends BaseStructure {
|
||||||
routing: {
|
routing: {
|
||||||
route: "",
|
route: "",
|
||||||
roleId: "BASIC",
|
roleId: "BASIC",
|
||||||
|
homeScreen: false,
|
||||||
},
|
},
|
||||||
name: "screen-id",
|
name: "screen-id",
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
onSelect(block)
|
onSelect(block)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
<Icon hoverable name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,27 +67,20 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<Tabs quiet noPadding selected="Input">
|
<Tabs noHorizPadding selected="Input">
|
||||||
<Tab title="Input">
|
<Tab title="Input">
|
||||||
<div style="padding: 10px 10px 10px 10px;">
|
<TextArea
|
||||||
<TextArea
|
minHeight="80px"
|
||||||
minHeight="80px"
|
disabled
|
||||||
disabled
|
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
|
||||||
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
|
/>
|
||||||
/>
|
</Tab>
|
||||||
</div></Tab
|
|
||||||
>
|
|
||||||
<Tab title="Output">
|
<Tab title="Output">
|
||||||
<div style="padding: 10px 10px 10px 10px;">
|
<TextArea
|
||||||
<TextArea
|
minHeight="100px"
|
||||||
minHeight="100px"
|
disabled
|
||||||
disabled
|
value={textArea(filteredResults?.[idx]?.outputs, "No output")}
|
||||||
value={textArea(
|
/>
|
||||||
filteredResults?.[idx]?.outputs,
|
|
||||||
"No output"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,6 +106,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
|
|
|
@ -19,12 +19,23 @@
|
||||||
notifications.error("Error deleting automation")
|
notifications.error("Error deleting automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function duplicateAutomation() {
|
||||||
|
try {
|
||||||
|
await automationStore.actions.duplicate(automation)
|
||||||
|
notifications.success("Automation has been duplicated successfully")
|
||||||
|
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error duplicating automation")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<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="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>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
import CronBuilder from "./CronBuilder.svelte"
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import { getSchemaForTable } from "builderStore/dataBinding"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ModalContent, Select } from "@budibase/bbui"
|
import { ModalContent, Select, Link } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
|
@ -87,6 +88,15 @@
|
||||||
onConfirm={saveRow}
|
onConfirm={saveRow}
|
||||||
>
|
>
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
|
<!-- need to explain to the user the readonly fields -->
|
||||||
|
{#if !creating}
|
||||||
|
<div>
|
||||||
|
A user's email, role, first and last names cannot be changed from within
|
||||||
|
the app builder. Please go to the <Link
|
||||||
|
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
|
||||||
|
> to do this.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<RowFieldControl
|
<RowFieldControl
|
||||||
meta={{ ...tableSchema.email, name: "Email" }}
|
meta={{ ...tableSchema.email, name: "Email" }}
|
||||||
bind:value={row.email}
|
bind:value={row.email}
|
||||||
|
|
|
@ -157,7 +157,8 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.datasource-icon {
|
.datasource-icon {
|
||||||
margin-right: 3px;
|
display: grid;
|
||||||
padding-top: 3px;
|
place-items: center;
|
||||||
|
flex: 0 0 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { allScreens, store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { tables, datasources } from "stores/backend"
|
import { tables, datasources } from "stores/backend"
|
||||||
import {
|
import {
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
$: allowDeletion = !external || table?.created
|
$: allowDeletion = !external || table?.created
|
||||||
|
|
||||||
function showDeleteModal() {
|
function showDeleteModal() {
|
||||||
templateScreens = $allScreens.filter(
|
templateScreens = $store.screens.filter(
|
||||||
screen => screen.autoTableId === table._id
|
screen => screen.autoTableId === table._id
|
||||||
)
|
)
|
||||||
willBeDeleted = ["All table data"].concat(
|
willBeDeleted = ["All table data"].concat(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon, StatusLight } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
|
@ -13,6 +13,9 @@
|
||||||
export let draggable = false
|
export let draggable = false
|
||||||
export let iconText
|
export let iconText
|
||||||
export let iconColor
|
export let iconColor
|
||||||
|
export let scrollable = false
|
||||||
|
export let color
|
||||||
|
export let highlighted = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -43,7 +46,10 @@
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
class:border
|
class:border
|
||||||
class:selected
|
class:selected
|
||||||
style={`padding-left: ${20 + indentLevel * 14}px`}
|
class:withActions
|
||||||
|
class:scrollable
|
||||||
|
class:highlighted
|
||||||
|
style={`padding-left: calc(${indentLevel * 14}px)`}
|
||||||
{draggable}
|
{draggable}
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
|
@ -55,7 +61,13 @@
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
<div
|
||||||
|
class:opened
|
||||||
|
class:relative={indentLevel === 0}
|
||||||
|
class:absolute={indentLevel > 0}
|
||||||
|
class="icon arrow"
|
||||||
|
on:click={onIconClick}
|
||||||
|
>
|
||||||
<Icon size="S" name="ChevronRight" />
|
<Icon size="S" name="ChevronRight" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -76,6 +88,11 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if color}
|
||||||
|
<div class="light">
|
||||||
|
<StatusLight size="L" {color} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,24 +102,31 @@
|
||||||
color: var(--grey-7);
|
color: var(--grey-7);
|
||||||
transition: background-color
|
transition: background-color
|
||||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||||
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
padding: 0 var(--spacing-l) 0;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-item.scrollable {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
.nav-item.highlighted {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
.nav-item.selected {
|
.nav-item.selected {
|
||||||
background-color: var(--grey-2);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background-color: var(--grey-3);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.nav-item:hover .actions {
|
.nav-item:hover .actions {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item-content {
|
.nav-item-content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -111,51 +135,84 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Needed to fully display the actions icon */
|
||||||
|
.nav-item.scrollable .nav-item-content {
|
||||||
|
padding-right: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 16px;
|
flex: 0 0 24px;
|
||||||
flex: 0 0 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
.icon.arrow {
|
.icon.arrow {
|
||||||
margin: 0 -2px 0 -6px;
|
flex: 0 0 20px;
|
||||||
font-size: 12px;
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.icon.arrow.absolute {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
.icon.arrow :global(svg) {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
.icon.arrow.relative {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 -6px 0 -4px;
|
||||||
}
|
}
|
||||||
.icon.arrow.opened {
|
.icon.arrow.opened {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
.icon + .icon {
|
|
||||||
margin-left: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
visibility: hidden;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconText {
|
.iconText {
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
font-size: var(--spectrum-global-dimension-font-size-50);
|
font-size: var(--spectrum-global-dimension-font-size-50);
|
||||||
flex: 0 0 34px;
|
flex: 0 0 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
color: var(--spectrum-global-color-gray-800);
|
||||||
|
}
|
||||||
|
.scrollable .text {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.actions,
|
||||||
|
.light :global(.spectrum-StatusLight) {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.light {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.nav-item.withActions:hover .light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button secondary on:click={publishModal.show}>Publish</Button>
|
<Button cta on:click={publishModal.show}>Publish</Button>
|
||||||
<Modal bind:this={feedbackModal}>
|
<Modal bind:this={feedbackModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Enjoying Budibase?"
|
title="Enjoying Budibase?"
|
||||||
|
|
|
@ -28,7 +28,13 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Icon name="Revert" hoverable on:click={revertModal.show} />
|
<Icon
|
||||||
|
name="Revert"
|
||||||
|
hoverable
|
||||||
|
on:click={revertModal.show}
|
||||||
|
tooltip="Revert changes"
|
||||||
|
dataCy="revert-application-topnav"
|
||||||
|
/>
|
||||||
<Modal bind:this={revertModal}>
|
<Modal bind:this={revertModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Revert Changes"
|
title="Revert Changes"
|
||||||
|
|
|
@ -69,7 +69,14 @@
|
||||||
|
|
||||||
{#if !hideIcon}
|
{#if !hideIcon}
|
||||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
||||||
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
<Icon
|
||||||
|
name="Refresh"
|
||||||
|
hoverable
|
||||||
|
on:click={updateModal.show}
|
||||||
|
tooltip={updateAvailable
|
||||||
|
? "An update is available"
|
||||||
|
: "No updates are available"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
<script>
|
|
||||||
import { notifications, Select } from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
const themeOptions = [
|
|
||||||
{
|
|
||||||
label: "Lightest",
|
|
||||||
value: "spectrum--lightest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Light",
|
|
||||||
value: "spectrum--light",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Dark",
|
|
||||||
value: "spectrum--dark",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Darkest",
|
|
||||||
value: "spectrum--darkest",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const onChangeTheme = async theme => {
|
|
||||||
try {
|
|
||||||
await store.actions.theme.save(theme)
|
|
||||||
await store.actions.customTheme.save({
|
|
||||||
...get(store).customTheme,
|
|
||||||
navBackground:
|
|
||||||
theme === "spectrum--light"
|
|
||||||
? "var(--spectrum-global-color-gray-50)"
|
|
||||||
: "var(--spectrum-global-color-gray-100)",
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating theme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
value={$store.theme}
|
|
||||||
options={themeOptions}
|
|
||||||
placeholder={null}
|
|
||||||
on:change={e => onChangeTheme(e.detail)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,114 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ActionMenu,
|
|
||||||
ActionButton,
|
|
||||||
MenuItem,
|
|
||||||
Icon,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store, currentAssetName, selectedComponent } from "builderStore"
|
|
||||||
import structure from "./componentStructure.json"
|
|
||||||
|
|
||||||
$: enrichedStructure = enrichStructure(structure, $store.components)
|
|
||||||
|
|
||||||
const isChildAllowed = ({ name }, selectedComponent) => {
|
|
||||||
const currentComponent = store.actions.components.getDefinition(
|
|
||||||
selectedComponent?._component
|
|
||||||
)
|
|
||||||
return currentComponent?.illegalChildren?.includes(name.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
const enrichStructure = (structure, definitions) => {
|
|
||||||
let enrichedStructure = []
|
|
||||||
structure.forEach(item => {
|
|
||||||
if (typeof item === "string") {
|
|
||||||
const def = definitions[`@budibase/standard-components/${item}`]
|
|
||||||
if (def) {
|
|
||||||
enrichedStructure.push({
|
|
||||||
...def,
|
|
||||||
isCategory: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enrichedStructure.push({
|
|
||||||
...item,
|
|
||||||
isCategory: true,
|
|
||||||
children: enrichStructure(item.children || [], definitions),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return enrichedStructure
|
|
||||||
}
|
|
||||||
|
|
||||||
const onItemChosen = async item => {
|
|
||||||
if (!item.isCategory) {
|
|
||||||
try {
|
|
||||||
await store.actions.components.create(item.component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="components">
|
|
||||||
{#each enrichedStructure as item}
|
|
||||||
<ActionMenu disabled={!item.isCategory}>
|
|
||||||
<ActionButton
|
|
||||||
icon={item.icon}
|
|
||||||
disabled={!item.isCategory && isChildAllowed(item, $selectedComponent)}
|
|
||||||
quiet
|
|
||||||
size="S"
|
|
||||||
slot="control"
|
|
||||||
dataCy={`category-${item.name}`}
|
|
||||||
on:click={() => onItemChosen(item)}
|
|
||||||
>
|
|
||||||
<div class="buttonContent">
|
|
||||||
{item.name}
|
|
||||||
{#if item.isCategory}
|
|
||||||
<Icon size="S" name="ChevronDown" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{#each item.children || [] as item}
|
|
||||||
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
|
|
||||||
<MenuItem
|
|
||||||
dataCy={`component-${item.name}`}
|
|
||||||
icon={item.icon}
|
|
||||||
on:click={() => onItemChosen(item)}
|
|
||||||
disabled={isChildAllowed(item, $selectedComponent)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</MenuItem>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ActionMenu>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.components {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.components :global(> *) {
|
|
||||||
height: 32px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
.buttonContent :global(svg) {
|
|
||||||
margin-left: 2px !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,156 +0,0 @@
|
||||||
<script>
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import {
|
|
||||||
ActionButton,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
ColorPicker,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
Button,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
|
||||||
|
|
||||||
let modal
|
|
||||||
|
|
||||||
const defaultTheme = {
|
|
||||||
primaryColor: "var(--spectrum-global-color-blue-600)",
|
|
||||||
primaryColorHover: "var(--spectrum-global-color-blue-500)",
|
|
||||||
buttonBorderRadius: "16px",
|
|
||||||
navBackground: "var(--spectrum-global-color-gray-50)",
|
|
||||||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonBorderRadiusOptions = [
|
|
||||||
{
|
|
||||||
label: "None",
|
|
||||||
value: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Small",
|
|
||||||
value: "4px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Medium",
|
|
||||||
value: "8px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Large",
|
|
||||||
value: "16px",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const updateProperty = property => {
|
|
||||||
return async e => {
|
|
||||||
try {
|
|
||||||
store.actions.customTheme.save({
|
|
||||||
...get(store).customTheme,
|
|
||||||
[property]: e.detail,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating custom theme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetTheme = () => {
|
|
||||||
try {
|
|
||||||
const theme = get(store).theme
|
|
||||||
store.actions.customTheme.save({
|
|
||||||
...defaultTheme,
|
|
||||||
navBackground:
|
|
||||||
theme === "spectrum--light"
|
|
||||||
? "var(--spectrum-global-color-gray-50)"
|
|
||||||
: "var(--spectrum-global-color-gray-100)",
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error saving custom theme")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<ActionButton icon="Brush" on:click={modal.show}>Theme</ActionButton>
|
|
||||||
</div>
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<ModalContent
|
|
||||||
showConfirmButton={false}
|
|
||||||
cancelText="View changes"
|
|
||||||
showCloseIcon={false}
|
|
||||||
title="Theme settings"
|
|
||||||
>
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Theme</Label>
|
|
||||||
<AppThemeSelect />
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Button roundness</Label>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<Select
|
|
||||||
placeholder={null}
|
|
||||||
value={$store.customTheme?.buttonBorderRadius ||
|
|
||||||
defaultTheme.buttonBorderRadius}
|
|
||||||
on:change={updateProperty("buttonBorderRadius")}
|
|
||||||
options={buttonBorderRadiusOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Accent color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor}
|
|
||||||
on:change={updateProperty("primaryColor")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Accent color (hover)</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.customTheme?.primaryColorHover ||
|
|
||||||
defaultTheme.primaryColorHover}
|
|
||||||
on:change={updateProperty("primaryColorHover")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Navigation bar background color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.customTheme?.navBackground ||
|
|
||||||
defaultTheme.navBackground}
|
|
||||||
on:change={updateProperty("navBackground")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<Label size="L">Navigation bar text color</Label>
|
|
||||||
<ColorPicker
|
|
||||||
spectrumTheme={$store.theme}
|
|
||||||
value={$store.customTheme?.navTextColor || defaultTheme.navTextColor}
|
|
||||||
on:change={updateProperty("navTextColor")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
<div slot="footer">
|
|
||||||
<Button secondary quiet on:click={resetTheme}>Reset</Button>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
.setting {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.select-wrapper {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from "./CurrentItemPreview.svelte"
|
|
|
@ -1,157 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
|
||||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import { capitalise } from "helpers"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import { selectedComponentPath } from "builderStore"
|
|
||||||
|
|
||||||
export let components = []
|
|
||||||
export let currentComponent
|
|
||||||
export let onSelect = () => {}
|
|
||||||
export let level = 0
|
|
||||||
export let dragDropStore
|
|
||||||
|
|
||||||
let closedNodes = {}
|
|
||||||
|
|
||||||
const selectComponent = component => {
|
|
||||||
store.actions.components.select(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragstart = component => e => {
|
|
||||||
e.dataTransfer.dropEffect = DropEffect.MOVE
|
|
||||||
dragDropStore.actions.dragstart(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragover = (component, index) => e => {
|
|
||||||
const definition = store.actions.components.getDefinition(
|
|
||||||
component._component
|
|
||||||
)
|
|
||||||
const canHaveChildrenButIsEmpty =
|
|
||||||
definition?.hasChildren && !component._children?.length
|
|
||||||
|
|
||||||
e.dataTransfer.dropEffect = DropEffect.COPY
|
|
||||||
|
|
||||||
// how far down the mouse pointer is on the drop target
|
|
||||||
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
|
|
||||||
|
|
||||||
dragDropStore.actions.dragover({
|
|
||||||
component,
|
|
||||||
index,
|
|
||||||
canHaveChildrenButIsEmpty,
|
|
||||||
mousePosition,
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const getComponentText = component => {
|
|
||||||
if (component._instanceName) {
|
|
||||||
return component._instanceName
|
|
||||||
}
|
|
||||||
const type =
|
|
||||||
component._component.replace("@budibase/standard-components/", "") ||
|
|
||||||
"component"
|
|
||||||
return capitalise(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleNodeOpen(componentId) {
|
|
||||||
if (closedNodes[componentId]) {
|
|
||||||
delete closedNodes[componentId]
|
|
||||||
} else {
|
|
||||||
closedNodes[componentId] = true
|
|
||||||
}
|
|
||||||
closedNodes = closedNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = async () => {
|
|
||||||
try {
|
|
||||||
await dragDropStore.actions.drop()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
|
||||||
if (!component?._children?.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (selectedComponentPath.includes(component._id)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return !closedNodes[component._id]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{#each components || [] as component, index (component._id)}
|
|
||||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
|
||||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
|
||||||
<div
|
|
||||||
on:drop={onDrop}
|
|
||||||
ondragover="return false"
|
|
||||||
ondragenter="return false"
|
|
||||||
class="drop-item"
|
|
||||||
style="margin-left: {(level + 1) * 16}px"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<NavItem
|
|
||||||
draggable
|
|
||||||
on:dragend={dragDropStore.actions.reset}
|
|
||||||
on:dragstart={dragstart(component)}
|
|
||||||
on:dragover={dragover(component, index)}
|
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
|
||||||
on:drop={onDrop}
|
|
||||||
text={getComponentText(component)}
|
|
||||||
withArrow
|
|
||||||
indentLevel={level + 1}
|
|
||||||
selected={$store.selectedComponentId === component._id}
|
|
||||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
|
||||||
>
|
|
||||||
<ComponentDropdownMenu {component} />
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
|
||||||
<svelte:self
|
|
||||||
components={component._children}
|
|
||||||
{currentComponent}
|
|
||||||
{onSelect}
|
|
||||||
{dragDropStore}
|
|
||||||
level={level + 1}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)}
|
|
||||||
<div
|
|
||||||
on:drop={onDrop}
|
|
||||||
ondragover="return false"
|
|
||||||
ondragenter="return false"
|
|
||||||
class="drop-item"
|
|
||||||
style="margin-left: {(level +
|
|
||||||
($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) *
|
|
||||||
16}px"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
ul,
|
|
||||||
li {
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-item {
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
height: 32px;
|
|
||||||
background: var(--grey-3);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,74 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import {
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Input,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
|
|
||||||
export let layout
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
let editLayoutNameModal
|
|
||||||
let name = layout.name
|
|
||||||
|
|
||||||
const deleteLayout = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.layouts.delete(layout)
|
|
||||||
notifications.success("Layout deleted successfully")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error deleting layout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveLayout = async () => {
|
|
||||||
try {
|
|
||||||
const layoutToSave = cloneDeep(layout)
|
|
||||||
layoutToSave.name = name
|
|
||||||
await store.actions.layouts.save(layoutToSave)
|
|
||||||
notifications.success("Layout saved successfully")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error saving layout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionMenu>
|
|
||||||
<div slot="control" class="icon">
|
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
|
||||||
</div>
|
|
||||||
<MenuItem icon="Edit" on:click={editLayoutNameModal.show}>Edit</MenuItem>
|
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
|
||||||
</ActionMenu>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
body={"Are you sure you wish to delete this layout?"}
|
|
||||||
okText="Delete layout"
|
|
||||||
onOk={deleteLayout}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal bind:this={editLayoutNameModal}>
|
|
||||||
<ModalContent
|
|
||||||
title="Edit Layout Name"
|
|
||||||
confirmText="Save"
|
|
||||||
onConfirm={saveLayout}
|
|
||||||
disabled={!name}
|
|
||||||
>
|
|
||||||
<Input thin type="text" label="Name" bind:value={name} />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,82 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import {
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
Icon,
|
|
||||||
Layout,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
export let path
|
|
||||||
export let screens
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
const deleteScreens = async () => {
|
|
||||||
if (!screens?.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (let { id } of screens) {
|
|
||||||
// We have to fetch the screen to be deleted immediately before deleting
|
|
||||||
// as otherwise we're very likely to 409
|
|
||||||
const screen = get(store).screens.find(screen => screen._id === id)
|
|
||||||
if (!screen) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await store.actions.screens.delete(screen)
|
|
||||||
}
|
|
||||||
notifications.success("Screens deleted successfully")
|
|
||||||
$goto("../")
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error deleting screens")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionMenu>
|
|
||||||
<div slot="control" class="icon">
|
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
|
||||||
</div>
|
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
|
||||||
Delete all screens
|
|
||||||
</MenuItem>
|
|
||||||
</ActionMenu>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
okText="Delete screens"
|
|
||||||
onOk={deleteScreens}
|
|
||||||
>
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div>
|
|
||||||
Are you sure you want to delete all screens under the <b>{path}</b> route?
|
|
||||||
</div>
|
|
||||||
<div>The following screens will be deleted:</div>
|
|
||||||
<div class="to-delete">
|
|
||||||
{#each screens as screen}
|
|
||||||
<div>{screen.route}</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.to-delete {
|
|
||||||
font-weight: bold;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-left: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,106 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
store,
|
|
||||||
selectedComponent,
|
|
||||||
currentAsset,
|
|
||||||
screenSearchString,
|
|
||||||
} from "builderStore"
|
|
||||||
import instantiateStore from "./dragDropStore"
|
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
const ROUTE_NAME_MAP = {
|
|
||||||
"/": {
|
|
||||||
BASIC: "Home",
|
|
||||||
PUBLIC: "Home",
|
|
||||||
ADMIN: "Home",
|
|
||||||
POWER: "Home",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragDropStore = instantiateStore()
|
|
||||||
|
|
||||||
export let route
|
|
||||||
export let path
|
|
||||||
export let indent
|
|
||||||
export let border
|
|
||||||
|
|
||||||
let routeManuallyOpened = false
|
|
||||||
|
|
||||||
$: selectedScreen = $currentAsset
|
|
||||||
$: allScreens = getAllScreens(route)
|
|
||||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
|
||||||
$: hasSearchMatch = $screenSearchString && filteredScreens.length > 0
|
|
||||||
$: noSearchMatch = $screenSearchString && !filteredScreens.length
|
|
||||||
$: routeSelected =
|
|
||||||
route.subpaths[selectedScreen?.routing?.route] !== undefined
|
|
||||||
$: routeOpened = routeManuallyOpened || routeSelected || hasSearchMatch
|
|
||||||
|
|
||||||
const changeScreen = screenId => {
|
|
||||||
store.actions.screens.select(screenId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllScreens = route => {
|
|
||||||
let screens = []
|
|
||||||
Object.entries(route.subpaths).forEach(([route, subpath]) => {
|
|
||||||
Object.entries(subpath.screens).forEach(([role, id]) => {
|
|
||||||
screens.push({ id, route, role })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return screens
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFilteredScreens = (screens, searchString) => {
|
|
||||||
return screens.filter(
|
|
||||||
screen => !searchString || screen.route.includes(searchString)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleManuallyOpened = () => {
|
|
||||||
if (get(screenSearchString)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
routeManuallyOpened = !routeManuallyOpened
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !noSearchMatch}
|
|
||||||
<NavItem
|
|
||||||
icon="FolderOutline"
|
|
||||||
text={path}
|
|
||||||
on:click={toggleManuallyOpened}
|
|
||||||
opened={routeOpened}
|
|
||||||
{border}
|
|
||||||
withArrow={route.subpaths}
|
|
||||||
>
|
|
||||||
<PathDropdownMenu screens={allScreens} {path} />
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
{#if routeOpened}
|
|
||||||
{#each filteredScreens as screen (screen.id)}
|
|
||||||
<NavItem
|
|
||||||
icon="WebPage"
|
|
||||||
indentLevel={indent || 1}
|
|
||||||
selected={$store.selectedScreenId === screen.id &&
|
|
||||||
$store.currentView === "detail"}
|
|
||||||
opened={$store.selectedScreenId === screen.id}
|
|
||||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
|
||||||
withArrow={route.subpaths}
|
|
||||||
on:click={() => changeScreen(screen.id)}
|
|
||||||
>
|
|
||||||
<ScreenDropdownMenu screenId={screen.id} />
|
|
||||||
</NavItem>
|
|
||||||
{#if selectedScreen?._id === screen.id}
|
|
||||||
<ComponentTree
|
|
||||||
level={1}
|
|
||||||
components={selectedScreen.props._children}
|
|
||||||
currentComponent={$selectedComponent}
|
|
||||||
{dragDropStore}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
|
@ -1,104 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
import { store as frontendStore } from "builderStore"
|
|
||||||
import { findComponentPath } from "builderStore/componentUtils"
|
|
||||||
|
|
||||||
export const DropEffect = {
|
|
||||||
MOVE: "move",
|
|
||||||
COPY: "copy",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DropPosition = {
|
|
||||||
ABOVE: "above",
|
|
||||||
BELOW: "below",
|
|
||||||
INSIDE: "inside",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const store = writable({})
|
|
||||||
|
|
||||||
store.actions = {
|
|
||||||
dragstart: component => {
|
|
||||||
store.update(state => {
|
|
||||||
state.dragged = component
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
dragover: ({
|
|
||||||
component,
|
|
||||||
index,
|
|
||||||
canHaveChildrenButIsEmpty,
|
|
||||||
mousePosition,
|
|
||||||
}) => {
|
|
||||||
store.update(state => {
|
|
||||||
state.targetComponent = component
|
|
||||||
// only allow dropping inside when container is empty
|
|
||||||
// if container has children, drag over them
|
|
||||||
|
|
||||||
if (canHaveChildrenButIsEmpty && index === 0) {
|
|
||||||
// hovered above center of target
|
|
||||||
if (mousePosition < 0.4) {
|
|
||||||
state.dropPosition = DropPosition.ABOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
// hovered around bottom of target
|
|
||||||
if (mousePosition > 0.8) {
|
|
||||||
state.dropPosition = DropPosition.BELOW
|
|
||||||
}
|
|
||||||
|
|
||||||
// hovered in center of target
|
|
||||||
if (mousePosition > 0.4 && mousePosition < 0.8) {
|
|
||||||
state.dropPosition = DropPosition.INSIDE
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
// bottom half
|
|
||||||
if (mousePosition > 0.5) {
|
|
||||||
state.dropPosition = DropPosition.BELOW
|
|
||||||
} else {
|
|
||||||
state.dropPosition = canHaveChildrenButIsEmpty
|
|
||||||
? DropPosition.INSIDE
|
|
||||||
: DropPosition.ABOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
reset: () => {
|
|
||||||
store.update(state => {
|
|
||||||
state.dropPosition = ""
|
|
||||||
state.targetComponent = null
|
|
||||||
state.dragged = null
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
drop: async () => {
|
|
||||||
const state = get(store)
|
|
||||||
|
|
||||||
// Stop if the target and source are the same
|
|
||||||
if (state.targetComponent === state.dragged) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Stop if the target or source are null
|
|
||||||
if (!state.targetComponent || !state.dragged) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Stop if the target is a child of source
|
|
||||||
const path = findComponentPath(state.dragged, state.targetComponent._id)
|
|
||||||
const ids = path.map(component => component._id)
|
|
||||||
if (ids.includes(state.targetComponent._id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut and paste the component
|
|
||||||
frontendStore.actions.components.copy(state.dragged, true)
|
|
||||||
await frontendStore.actions.components.paste(
|
|
||||||
state.targetComponent,
|
|
||||||
state.dropPosition
|
|
||||||
)
|
|
||||||
store.actions.reset()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return store
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store, selectedAccessRole } from "builderStore"
|
|
||||||
import PathTree from "./PathTree.svelte"
|
|
||||||
|
|
||||||
let routes = {}
|
|
||||||
let paths = []
|
|
||||||
|
|
||||||
$: allRoutes = $store.routes
|
|
||||||
$: selectedScreenId = $store.selectedScreenId
|
|
||||||
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId)
|
|
||||||
|
|
||||||
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => {
|
|
||||||
const sortedPaths = Object.keys(allRoutes || {}).sort()
|
|
||||||
|
|
||||||
let found = false
|
|
||||||
let firstValidScreenId
|
|
||||||
let filteredRoutes = {}
|
|
||||||
let screenRoleId
|
|
||||||
|
|
||||||
// Filter all routes down to only those which match the current role
|
|
||||||
sortedPaths.forEach(path => {
|
|
||||||
const config = allRoutes[path]
|
|
||||||
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => {
|
|
||||||
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => {
|
|
||||||
if (screenId === selectedScreenId) {
|
|
||||||
screenRoleId = roleId
|
|
||||||
found = roleId === selectedRoleId
|
|
||||||
}
|
|
||||||
if (roleId === selectedRoleId) {
|
|
||||||
if (!firstValidScreenId) {
|
|
||||||
firstValidScreenId = screenId
|
|
||||||
}
|
|
||||||
if (!filteredRoutes[path]) {
|
|
||||||
filteredRoutes[path] = { subpaths: {} }
|
|
||||||
}
|
|
||||||
filteredRoutes[path].subpaths[subpath] = {
|
|
||||||
screens: {
|
|
||||||
[selectedRoleId]: screenId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
routes = { ...filteredRoutes }
|
|
||||||
paths = Object.keys(routes || {}).sort()
|
|
||||||
|
|
||||||
// Select the correct role for the current screen ID
|
|
||||||
if (!found && screenRoleId) {
|
|
||||||
selectedAccessRole.set(screenRoleId)
|
|
||||||
if (screenRoleId !== selectedRoleId) {
|
|
||||||
updatePaths(allRoutes, screenRoleId, selectedScreenId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the selected screen isn't in this filtered list, select the first one
|
|
||||||
else if (!found && firstValidScreenId) {
|
|
||||||
store.actions.screens.select(firstValidScreenId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="root" class:has-screens={!!paths?.length}>
|
|
||||||
{#each paths as path, idx (path)}
|
|
||||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
|
||||||
{/each}
|
|
||||||
{#if !paths.length}
|
|
||||||
<div class="empty">
|
|
||||||
There aren't any screens configured with this access role.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root.has-screens {
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
div.empty {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
color: var(--grey-5);
|
|
||||||
padding: var(--spacing-xs) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,229 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount, setContext } from "svelte"
|
|
||||||
import { goto, params } from "@roxi/routify"
|
|
||||||
import {
|
|
||||||
store,
|
|
||||||
allScreens,
|
|
||||||
selectedAccessRole,
|
|
||||||
screenSearchString,
|
|
||||||
} from "builderStore"
|
|
||||||
import { roles } from "stores/backend"
|
|
||||||
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
|
|
||||||
import Layout from "components/design/NavigationPanel/Layout.svelte"
|
|
||||||
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
|
|
||||||
import {
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
Search,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
Layout as BBUILayout,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let showModal
|
|
||||||
|
|
||||||
let scrollRef
|
|
||||||
|
|
||||||
const scrollTo = bounds => {
|
|
||||||
if (!bounds) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarWidth = 259
|
|
||||||
const navItemHeight = 32
|
|
||||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
|
||||||
|
|
||||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
|
||||||
let newOffsets = {}
|
|
||||||
|
|
||||||
// Calculate left offset
|
|
||||||
const offsetX = bounds.left + bounds.width + scrollLeft + 20
|
|
||||||
if (offsetX > sidebarWidth) {
|
|
||||||
newOffsets.left = offsetX - sidebarWidth
|
|
||||||
} else {
|
|
||||||
newOffsets.left = 0
|
|
||||||
}
|
|
||||||
if (newOffsets.left === scrollLeft) {
|
|
||||||
delete newOffsets.left
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate top offset
|
|
||||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
|
||||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
|
||||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
|
||||||
} else if (offsetY < scrollTop + navItemHeight) {
|
|
||||||
newOffsets.top = offsetY - navItemHeight
|
|
||||||
} else {
|
|
||||||
delete newOffsets.top
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if offset is unchanged
|
|
||||||
if (newOffsets.left == null && newOffsets.top == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smoothly scroll to the offset
|
|
||||||
scrollRef.scroll({
|
|
||||||
...newOffsets,
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext("scroll", {
|
|
||||||
scrollTo,
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
title: "Screens",
|
|
||||||
key: "screen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Layouts",
|
|
||||||
key: "layout",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
let newLayoutModal
|
|
||||||
$: selected = tabs.find(t => t.key === $params.assetType)?.title || "Screens"
|
|
||||||
|
|
||||||
const navigate = ({ detail }) => {
|
|
||||||
const { key } = tabs.find(t => t.title === detail)
|
|
||||||
$goto(`../${key}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateAccessRole = event => {
|
|
||||||
const role = event.detail
|
|
||||||
|
|
||||||
// Select a valid screen with this new role - otherwise we'll not be
|
|
||||||
// able to change role at all because ComponentNavigationTree will kick us
|
|
||||||
// back the current role again because the same screen ID is still selected
|
|
||||||
const firstValidScreenId = $allScreens.find(
|
|
||||||
screen => screen.routing.roleId === role
|
|
||||||
)?._id
|
|
||||||
if (firstValidScreenId) {
|
|
||||||
store.actions.screens.select(firstValidScreenId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise clear the selected screen ID so that the first new valid screen
|
|
||||||
// can be selected by ComponentNavigationTree
|
|
||||||
else {
|
|
||||||
store.update(state => {
|
|
||||||
state.selectedScreenId = null
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedAccessRole.set(role)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.routing.fetch()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error fetching routes")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
<Tabs {selected} on:select={navigate}>
|
|
||||||
<Tab title="Screens">
|
|
||||||
<div class="tab-content-padding">
|
|
||||||
<BBUILayout noPadding gap="XS">
|
|
||||||
<Select
|
|
||||||
on:change={updateAccessRole}
|
|
||||||
value={$selectedAccessRole}
|
|
||||||
label="Filter by Access"
|
|
||||||
getOptionLabel={role => role.name}
|
|
||||||
getOptionValue={role => role._id}
|
|
||||||
options={$roles}
|
|
||||||
/>
|
|
||||||
<Search
|
|
||||||
placeholder="Enter a route to search"
|
|
||||||
label="Search Screens"
|
|
||||||
bind:value={$screenSearchString}
|
|
||||||
/>
|
|
||||||
</BBUILayout>
|
|
||||||
<div class="nav-items-container" bind:this={scrollRef}>
|
|
||||||
<ComponentNavigationTree />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Layouts">
|
|
||||||
<div class="tab-content-padding">
|
|
||||||
<div
|
|
||||||
class="nav-items-container nav-items-container--layouts"
|
|
||||||
bind:this={scrollRef}
|
|
||||||
>
|
|
||||||
<div class="layouts-container">
|
|
||||||
{#each $store.layouts as layout, idx (layout._id)}
|
|
||||||
<Layout {layout} border={idx > 0} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Modal bind:this={newLayoutModal}>
|
|
||||||
<NewLayoutModal />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
<div class="add-button">
|
|
||||||
<Icon
|
|
||||||
hoverable
|
|
||||||
name="AddCircle"
|
|
||||||
on:click={selected === "Layouts" ? newLayoutModal.show() : showModal()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
position: relative;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.title :global(.spectrum-Tabs-content),
|
|
||||||
.title :global(.spectrum-Tabs-content > div),
|
|
||||||
.title :global(.spectrum-Tabs-content > div > div) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-button {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--spacing-l);
|
|
||||||
right: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content-padding {
|
|
||||||
padding: 0 var(--spacing-xl);
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-items-container {
|
|
||||||
border-top: var(--border-light);
|
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
|
||||||
padding: var(--spacing-m) 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
height: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.nav-items-container--layouts {
|
|
||||||
border-top: none;
|
|
||||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
|
||||||
}
|
|
||||||
|
|
||||||
.layouts-container {
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<script>
|
|
||||||
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
|
|
||||||
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte"
|
|
||||||
import initDragDropStore from "./ComponentNavigationTree/dragDropStore"
|
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
|
||||||
import { store, selectedComponent } from "builderStore"
|
|
||||||
|
|
||||||
export let layout
|
|
||||||
export let border
|
|
||||||
|
|
||||||
const dragDropStore = initDragDropStore()
|
|
||||||
|
|
||||||
const selectLayout = () => {
|
|
||||||
store.actions.layouts.select(layout._id)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavItem
|
|
||||||
{border}
|
|
||||||
icon="ClassicGridView"
|
|
||||||
text={layout.name}
|
|
||||||
withArrow
|
|
||||||
selected={$store.selectedLayoutId === layout._id}
|
|
||||||
opened={$store.selectedLayoutId === layout._id}
|
|
||||||
on:click={selectLayout}
|
|
||||||
>
|
|
||||||
<LayoutDropdownMenu {layout} />
|
|
||||||
</NavItem>
|
|
||||||
|
|
||||||
{#if $store.selectedLayoutId === layout._id && layout.props?._children}
|
|
||||||
<ComponentTree
|
|
||||||
components={layout.props._children}
|
|
||||||
currentComponent={$selectedComponent}
|
|
||||||
{dragDropStore}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
|
@ -1,93 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ModalContent, Body, Detail } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let selectedScreens
|
|
||||||
export let chooseModal
|
|
||||||
export let save
|
|
||||||
let selectedNav
|
|
||||||
let createdScreens = []
|
|
||||||
$: blankSelected = selectedScreens.length === 1
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Select navigation"
|
|
||||||
cancelText="Back"
|
|
||||||
onCancel={() => (blankSelected ? chooseModal(1) : chooseModal(0))}
|
|
||||||
size="M"
|
|
||||||
onConfirm={() => {
|
|
||||||
save(createdScreens)
|
|
||||||
}}
|
|
||||||
disabled={!selectedNav}
|
|
||||||
>
|
|
||||||
<Body size="S"
|
|
||||||
>Please select your preferred layout for the new application:</Body
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="wrapper">
|
|
||||||
<div
|
|
||||||
data-cy="left-nav"
|
|
||||||
on:click={() => (selectedNav = "Left")}
|
|
||||||
class:unselected={selectedNav && selectedNav !== "Left"}
|
|
||||||
>
|
|
||||||
<div class="box">
|
|
||||||
<div class="side-nav" />
|
|
||||||
</div>
|
|
||||||
<div><Detail>Side Nav</Detail></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
on:click={() => (selectedNav = "Top")}
|
|
||||||
class:unselected={selectedNav && selectedNav !== "Top"}
|
|
||||||
>
|
|
||||||
<div class="box">
|
|
||||||
<div class="top-nav" />
|
|
||||||
</div>
|
|
||||||
<div><Detail>Top Nav</Detail></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
on:click={() => (selectedNav = "None")}
|
|
||||||
class:unselected={selectedNav && selectedNav !== "None"}
|
|
||||||
>
|
|
||||||
<div class="box" />
|
|
||||||
<div><Detail>No Nav</Detail></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.side-nav {
|
|
||||||
float: left;
|
|
||||||
background: #d3d3d3 0% 0% no-repeat padding-box;
|
|
||||||
border-radius: 2px 0px 0px 2px;
|
|
||||||
height: 100%;
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-nav {
|
|
||||||
background: #d3d3d3 0% 0% no-repeat padding-box;
|
|
||||||
vertical-align: top;
|
|
||||||
width: 100%;
|
|
||||||
height: 15%;
|
|
||||||
}
|
|
||||||
.box {
|
|
||||||
display: inline-block;
|
|
||||||
background: #eaeaea 0% 0% no-repeat padding-box;
|
|
||||||
border: 1px solid #d3d3d3;
|
|
||||||
border-radius: 2px;
|
|
||||||
opacity: 1;
|
|
||||||
width: 120px;
|
|
||||||
height: 70px;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: flex;
|
|
||||||
padding-top: 4%;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin-right: 5%;
|
|
||||||
}
|
|
||||||
.unselected {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<script>
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { Input, ModalContent } from "@budibase/bbui"
|
|
||||||
|
|
||||||
let name = ""
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
try {
|
|
||||||
await store.actions.layouts.save({ name })
|
|
||||||
notifications.success(`Layout ${name} created successfully`)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating layout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}>
|
|
||||||
<Input thin label="Name" bind:value={name} />
|
|
||||||
</ModalContent>
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Heading } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let icon
|
||||||
|
export let showAddButton = false
|
||||||
|
export let showBackButton = false
|
||||||
|
export let showExpandIcon = false
|
||||||
|
export let onClickAddButton
|
||||||
|
export let onClickBackButton
|
||||||
|
export let borderLeft = false
|
||||||
|
export let borderRight = false
|
||||||
|
|
||||||
|
let wide = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="panel" class:wide class:borderLeft class:borderRight>
|
||||||
|
<div class="header">
|
||||||
|
{#if showBackButton}
|
||||||
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
|
{/if}
|
||||||
|
{#if icon}
|
||||||
|
<Icon name={icon} />
|
||||||
|
{/if}
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="XXS">{title || ""}</Heading>
|
||||||
|
</div>
|
||||||
|
{#if showExpandIcon}
|
||||||
|
<Icon
|
||||||
|
name={wide ? "Minimize" : "Maximize"}
|
||||||
|
hoverable
|
||||||
|
on:click={() => (wide = !wide)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if showAddButton}
|
||||||
|
<div class="add-button" on:click={onClickAddButton}>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.panel {
|
||||||
|
width: 260px;
|
||||||
|
background: var(--background);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
transition: width 130ms ease-out;
|
||||||
|
}
|
||||||
|
.panel.borderLeft {
|
||||||
|
border-left: var(--border-light);
|
||||||
|
}
|
||||||
|
.panel.borderRight {
|
||||||
|
border-right: var(--border-light);
|
||||||
|
}
|
||||||
|
.panel.wide {
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.title :global(h1) {
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.add-button {
|
||||||
|
flex: 0 0 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-semantic-cta-color-background-default);
|
||||||
|
transition: background var(--spectrum-global-animation-duration-100, 130ms)
|
||||||
|
ease-out;
|
||||||
|
}
|
||||||
|
.add-button:hover {
|
||||||
|
background: var(--spectrum-semantic-cta-color-background-hover);
|
||||||
|
}
|
||||||
|
.add-button :global(svg) {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,63 +0,0 @@
|
||||||
<script>
|
|
||||||
import { store, selectedComponent, currentAsset } from "builderStore"
|
|
||||||
import { Tabs, Tab } from "@budibase/bbui"
|
|
||||||
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
|
|
||||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
|
||||||
import DesignSection from "./DesignSection.svelte"
|
|
||||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
|
||||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
|
||||||
import {
|
|
||||||
getBindableProperties,
|
|
||||||
getComponentBindableProperties,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
|
|
||||||
$: componentInstance = $selectedComponent
|
|
||||||
$: componentDefinition = store.actions.components.getDefinition(
|
|
||||||
$selectedComponent?._component
|
|
||||||
)
|
|
||||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
|
||||||
$: componentBindings = getComponentBindableProperties(
|
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Tabs selected="Settings" noPadding>
|
|
||||||
<Tab title="Settings">
|
|
||||||
<div class="container">
|
|
||||||
{#key componentInstance?._id}
|
|
||||||
<ScreenSettingsSection
|
|
||||||
{componentInstance}
|
|
||||||
{componentDefinition}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
<ComponentSettingsSection
|
|
||||||
{componentInstance}
|
|
||||||
{componentDefinition}
|
|
||||||
{bindings}
|
|
||||||
{componentBindings}
|
|
||||||
/>
|
|
||||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
|
||||||
<CustomStylesSection
|
|
||||||
{componentInstance}
|
|
||||||
{componentDefinition}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
<ConditionalUISection
|
|
||||||
{componentInstance}
|
|
||||||
{componentDefinition}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import NavigationDrawer from "./NavigationDrawer.svelte"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
|
|
||||||
export let value = []
|
|
||||||
let drawer
|
|
||||||
let links = cloneDeep(value || [])
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const save = () => {
|
|
||||||
dispatch("change", links)
|
|
||||||
drawer.hide()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Configure links</ActionButton>
|
|
||||||
<Drawer bind:this={drawer} title={"Navigation Links"}>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Configure the links in your navigation bar.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
|
||||||
<NavigationDrawer slot="body" bind:links />
|
|
||||||
</Drawer>
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
|
||||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
|
||||||
import S3DataSourceSelect from "./S3DataSourceSelect.svelte"
|
|
||||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
|
||||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
|
||||||
import TableSelect from "./TableSelect.svelte"
|
|
||||||
import ColorPicker from "./ColorPicker.svelte"
|
|
||||||
import { IconSelect } from "./IconSelect"
|
|
||||||
import FieldSelect from "./FieldSelect.svelte"
|
|
||||||
import MultiFieldSelect from "./MultiFieldSelect.svelte"
|
|
||||||
import SearchFieldSelect from "./SearchFieldSelect.svelte"
|
|
||||||
import SchemaSelect from "./SchemaSelect.svelte"
|
|
||||||
import SectionSelect from "./SectionSelect.svelte"
|
|
||||||
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
|
|
||||||
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
|
||||||
import URLSelect from "./URLSelect.svelte"
|
|
||||||
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
|
|
||||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
|
||||||
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
|
|
||||||
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
|
|
||||||
import ColumnEditor from "./ColumnEditor/ColumnEditor.svelte"
|
|
||||||
|
|
||||||
const componentMap = {
|
|
||||||
text: DrawerBindableCombobox,
|
|
||||||
select: Select,
|
|
||||||
dataSource: DataSourceSelect,
|
|
||||||
"dataSource/s3": S3DataSourceSelect,
|
|
||||||
dataProvider: DataProviderSelect,
|
|
||||||
boolean: Checkbox,
|
|
||||||
number: Stepper,
|
|
||||||
event: ButtonActionEditor,
|
|
||||||
table: TableSelect,
|
|
||||||
color: ColorPicker,
|
|
||||||
icon: IconSelect,
|
|
||||||
field: FieldSelect,
|
|
||||||
multifield: MultiFieldSelect,
|
|
||||||
searchfield: SearchFieldSelect,
|
|
||||||
options: OptionsEditor,
|
|
||||||
schema: SchemaSelect,
|
|
||||||
section: SectionSelect,
|
|
||||||
navigation: NavigationEditor,
|
|
||||||
filter: FilterEditor,
|
|
||||||
url: URLSelect,
|
|
||||||
columns: ColumnEditor,
|
|
||||||
"field/string": FormFieldSelect,
|
|
||||||
"field/number": FormFieldSelect,
|
|
||||||
"field/options": FormFieldSelect,
|
|
||||||
"field/boolean": FormFieldSelect,
|
|
||||||
"field/longform": FormFieldSelect,
|
|
||||||
"field/datetime": FormFieldSelect,
|
|
||||||
"field/attachment": FormFieldSelect,
|
|
||||||
"field/link": FormFieldSelect,
|
|
||||||
"field/array": FormFieldSelect,
|
|
||||||
"field/json": FormFieldSelect,
|
|
||||||
// Some validation types are the same as others, so not all types are
|
|
||||||
// explicitly listed here. e.g. options uses string validation
|
|
||||||
"validation/string": ValidationEditor,
|
|
||||||
"validation/array": ValidationEditor,
|
|
||||||
"validation/number": ValidationEditor,
|
|
||||||
"validation/boolean": ValidationEditor,
|
|
||||||
"validation/datetime": ValidationEditor,
|
|
||||||
"validation/attachment": ValidationEditor,
|
|
||||||
"validation/link": ValidationEditor,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getComponentForSettingType = type => {
|
|
||||||
return componentMap[type]
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
<script>
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { get as deepGet, setWith } from "lodash"
|
|
||||||
import { Input, DetailSummary, notifications } from "@budibase/bbui"
|
|
||||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
|
||||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
|
||||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
|
||||||
import { currentAsset, store } from "builderStore"
|
|
||||||
import { FrontendTypes } from "constants"
|
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
|
||||||
import { allScreens, selectedAccessRole } from "builderStore"
|
|
||||||
|
|
||||||
export let componentInstance
|
|
||||||
export let bindings
|
|
||||||
|
|
||||||
let errors = {}
|
|
||||||
|
|
||||||
const routeTaken = url => {
|
|
||||||
const roleId = get(selectedAccessRole) || "BASIC"
|
|
||||||
return get(allScreens).some(
|
|
||||||
screen =>
|
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
|
||||||
screen.routing.roleId === roleId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleTaken = roleId => {
|
|
||||||
const url = get(currentAsset)?.routing.route
|
|
||||||
return get(allScreens).some(
|
|
||||||
screen =>
|
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
|
||||||
screen.routing.roleId === roleId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAssetProps = (name, value, parser, validate) => {
|
|
||||||
if (parser) {
|
|
||||||
value = parser(value)
|
|
||||||
}
|
|
||||||
if (validate) {
|
|
||||||
const error = validate(value)
|
|
||||||
errors = {
|
|
||||||
...errors,
|
|
||||||
[name]: error,
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errors = {
|
|
||||||
...errors,
|
|
||||||
[name]: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedAsset = get(currentAsset)
|
|
||||||
store.update(state => {
|
|
||||||
if (
|
|
||||||
name === "_instanceName" &&
|
|
||||||
state.currentFrontEndType === FrontendTypes.SCREEN
|
|
||||||
) {
|
|
||||||
selectedAsset.props._instanceName = value
|
|
||||||
} else {
|
|
||||||
setWith(selectedAsset, name.split("."), value, Object)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
store.actions.preview.saveSelected()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error saving settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenSettings = [
|
|
||||||
{
|
|
||||||
key: "routing.route",
|
|
||||||
label: "Route",
|
|
||||||
control: Input,
|
|
||||||
parser: val => {
|
|
||||||
if (!val.startsWith("/")) {
|
|
||||||
val = "/" + val
|
|
||||||
}
|
|
||||||
return sanitizeUrl(val)
|
|
||||||
},
|
|
||||||
validate: val => {
|
|
||||||
const exisingValue = get(currentAsset)?.routing.route
|
|
||||||
if (val !== exisingValue && routeTaken(val)) {
|
|
||||||
return "That URL is already in use for this role"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "routing.roleId",
|
|
||||||
label: "Access",
|
|
||||||
control: RoleSelect,
|
|
||||||
validate: val => {
|
|
||||||
const exisingValue = get(currentAsset)?.routing.roleId
|
|
||||||
if (val !== exisingValue && roleTaken(val)) {
|
|
||||||
return "That role is already in use for this URL"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN}
|
|
||||||
<DetailSummary name="Screen" collapsible={false}>
|
|
||||||
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
|
|
||||||
<PropertyControl
|
|
||||||
control={def.control}
|
|
||||||
label={def.label}
|
|
||||||
key={def.key}
|
|
||||||
value={deepGet($currentAsset, def.key)}
|
|
||||||
onChange={val => setAssetProps(def.key, val, def.parser, def.validate)}
|
|
||||||
{bindings}
|
|
||||||
props={{ error: errors[def.key] }}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</DetailSummary>
|
|
||||||
{/if}
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||||
|
import DataSourceSelect from "./controls/DataSourceSelect.svelte"
|
||||||
|
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte"
|
||||||
|
import DataProviderSelect from "./controls/DataProviderSelect.svelte"
|
||||||
|
import ButtonActionEditor from "./controls/ButtonActionEditor/ButtonActionEditor.svelte"
|
||||||
|
import TableSelect from "./controls/TableSelect.svelte"
|
||||||
|
import ColorPicker from "./controls/ColorPicker.svelte"
|
||||||
|
import { IconSelect } from "./controls/IconSelect"
|
||||||
|
import FieldSelect from "./controls/FieldSelect.svelte"
|
||||||
|
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte"
|
||||||
|
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte"
|
||||||
|
import SchemaSelect from "./controls/SchemaSelect.svelte"
|
||||||
|
import SectionSelect from "./controls/SectionSelect.svelte"
|
||||||
|
import FilterEditor from "./controls/FilterEditor/FilterEditor.svelte"
|
||||||
|
import URLSelect from "./controls/URLSelect.svelte"
|
||||||
|
import OptionsEditor from "./controls/OptionsEditor/OptionsEditor.svelte"
|
||||||
|
import FormFieldSelect from "./controls/FormFieldSelect.svelte"
|
||||||
|
import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelte"
|
||||||
|
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
|
||||||
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
text: DrawerBindableCombobox,
|
||||||
|
select: Select,
|
||||||
|
dataSource: DataSourceSelect,
|
||||||
|
"dataSource/s3": S3DataSourceSelect,
|
||||||
|
dataProvider: DataProviderSelect,
|
||||||
|
boolean: Checkbox,
|
||||||
|
number: Stepper,
|
||||||
|
event: ButtonActionEditor,
|
||||||
|
table: TableSelect,
|
||||||
|
color: ColorPicker,
|
||||||
|
icon: IconSelect,
|
||||||
|
field: FieldSelect,
|
||||||
|
multifield: MultiFieldSelect,
|
||||||
|
searchfield: SearchFieldSelect,
|
||||||
|
options: OptionsEditor,
|
||||||
|
schema: SchemaSelect,
|
||||||
|
section: SectionSelect,
|
||||||
|
filter: FilterEditor,
|
||||||
|
url: URLSelect,
|
||||||
|
columns: ColumnEditor,
|
||||||
|
"field/string": FormFieldSelect,
|
||||||
|
"field/number": FormFieldSelect,
|
||||||
|
"field/options": FormFieldSelect,
|
||||||
|
"field/boolean": FormFieldSelect,
|
||||||
|
"field/longform": FormFieldSelect,
|
||||||
|
"field/datetime": FormFieldSelect,
|
||||||
|
"field/attachment": FormFieldSelect,
|
||||||
|
"field/link": FormFieldSelect,
|
||||||
|
"field/array": FormFieldSelect,
|
||||||
|
"field/json": FormFieldSelect,
|
||||||
|
// Some validation types are the same as others, so not all types are
|
||||||
|
// explicitly listed here. e.g. options uses string validation
|
||||||
|
"validation/string": ValidationEditor,
|
||||||
|
"validation/array": ValidationEditor,
|
||||||
|
"validation/number": ValidationEditor,
|
||||||
|
"validation/boolean": ValidationEditor,
|
||||||
|
"validation/datetime": ValidationEditor,
|
||||||
|
"validation/attachment": ValidationEditor,
|
||||||
|
"validation/link": ValidationEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComponentForSetting = setting => {
|
||||||
|
const { type, showInBar, barStyle } = setting || {}
|
||||||
|
if (!type) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can show a clone of the bar settings for certain select settings
|
||||||
|
if (showInBar && type === "select" && barStyle === "buttons") {
|
||||||
|
return BarButtonList
|
||||||
|
}
|
||||||
|
|
||||||
|
return componentMap[type]
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton, ActionGroup } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let onChange
|
||||||
|
export let options
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionGroup>
|
||||||
|
{#each options as option}
|
||||||
|
<ActionButton
|
||||||
|
icon={option.barIcon}
|
||||||
|
quiet
|
||||||
|
on:click={() => onChange(option.value)}
|
||||||
|
selected={option.value === value}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</ActionGroup>
|
|
@ -6,8 +6,8 @@
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
ActionMenu,
|
ActionButton,
|
||||||
MenuItem,
|
Search,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { getAvailableActions } from "./index"
|
import { getAvailableActions } from "./index"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
@ -22,8 +22,24 @@
|
||||||
export let actions
|
export let actions
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
|
||||||
|
$: showAvailableActions = !actions?.length
|
||||||
|
|
||||||
|
let actionQuery
|
||||||
|
$: parsedQuery =
|
||||||
|
typeof actionQuery === "string" ? actionQuery.toLowerCase().trim() : ""
|
||||||
|
|
||||||
let selectedAction = actions?.length ? actions[0] : null
|
let selectedAction = actions?.length ? actions[0] : null
|
||||||
|
|
||||||
|
$: mappedActionTypes = actionTypes.reduce((acc, action) => {
|
||||||
|
let parsedName = action.name.toLowerCase().trim()
|
||||||
|
if (parsedQuery.length && parsedName.indexOf(parsedQuery) < 0) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc[action.type] = acc[action.type] || []
|
||||||
|
acc[action.type].push(action)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
// These are ephemeral bindings which only exist while executing actions
|
// These are ephemeral bindings which only exist while executing actions
|
||||||
$: buttonContextBindings = getButtonContextBindings(
|
$: buttonContextBindings = getButtonContextBindings(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
|
@ -61,7 +77,12 @@
|
||||||
actions = actions
|
actions = actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAction = actionType => () => {
|
const toggleActionList = () => {
|
||||||
|
actionQuery = null
|
||||||
|
showAvailableActions = !showAvailableActions
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAction = actionType => {
|
||||||
const newAction = {
|
const newAction = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
[EVENT_TYPE_KEY]: actionType.name,
|
[EVENT_TYPE_KEY]: actionType.name,
|
||||||
|
@ -78,6 +99,11 @@
|
||||||
selectedAction = action
|
selectedAction = action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onAddAction = actionType => {
|
||||||
|
addAction(actionType)
|
||||||
|
toggleActionList()
|
||||||
|
}
|
||||||
|
|
||||||
function handleDndConsider(e) {
|
function handleDndConsider(e) {
|
||||||
actions = e.detail.items
|
actions = e.detail.items
|
||||||
}
|
}
|
||||||
|
@ -88,7 +114,39 @@
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<Layout noPadding gap="S" slot="sidebar">
|
<Layout noPadding gap="S" slot="sidebar">
|
||||||
{#if actions && actions.length > 0}
|
{#if showAvailableActions || !actions?.length}
|
||||||
|
<div class="actions-list">
|
||||||
|
{#if actions?.length > 0}
|
||||||
|
<div>
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
icon={"ArrowLeft"}
|
||||||
|
on:click={toggleActionList}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="search-wrap">
|
||||||
|
<Search placeholder="Search" bind:value={actionQuery} />
|
||||||
|
</div>
|
||||||
|
{#each Object.entries(mappedActionTypes) as [categoryId, category], idx}
|
||||||
|
<div class="heading" class:top-entry={idx === 0}>{categoryId}</div>
|
||||||
|
<ul>
|
||||||
|
{#each category as actionType}
|
||||||
|
<li on:click={onAddAction(actionType)}>
|
||||||
|
<span class="action-name">{actionType.name}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if actions && actions.length > 0 && !showAvailableActions}
|
||||||
|
<div>
|
||||||
|
<Button secondary on:click={toggleActionList}>Add Action</Button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="actions"
|
class="actions"
|
||||||
use:dndzone={{
|
use:dndzone={{
|
||||||
|
@ -120,17 +178,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<ActionMenu>
|
|
||||||
<Button slot="control" secondary>Add Action</Button>
|
|
||||||
{#each actionTypes as actionType}
|
|
||||||
<MenuItem on:click={addAction(actionType)}>
|
|
||||||
{actionType.name}
|
|
||||||
</MenuItem>
|
|
||||||
{/each}
|
|
||||||
</ActionMenu>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if selectedActionComponent}
|
{#if selectedActionComponent && !showAvailableActions}
|
||||||
{#key selectedAction.id}
|
{#key selectedAction.id}
|
||||||
<div class="selected-action-container">
|
<div class="selected-action-container">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
|
@ -152,13 +202,10 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-header {
|
.action-header {
|
||||||
color: var(--spectrum-global-color-gray-700);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-container {
|
.action-container {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
|
@ -182,4 +229,55 @@
|
||||||
.action-container.selected .action-header {
|
.action-container.selected .action-header {
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-list > * {
|
||||||
|
padding-bottom: var(--spectrum-global-dimension-static-size-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-list .heading {
|
||||||
|
padding-bottom: var(--spectrum-global-dimension-static-size-100);
|
||||||
|
padding-top: var(--spectrum-global-dimension-static-size-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-list .heading.top-entry {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||||
|
border-color 130ms ease-in-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
li:not(:last-of-type) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
li :global(*) {
|
||||||
|
transition: color 130ms ease-in-out;
|
||||||
|
}
|
||||||
|
li:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-name {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -69,9 +69,16 @@
|
||||||
notifications.error("Error creating automation")
|
notifications.error("Error creating automation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: actionCount = value?.length
|
||||||
|
$: actionText = `${actionCount || "No"} action${
|
||||||
|
actionCount !== 1 ? "s" : ""
|
||||||
|
} set`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="action-count">{actionText}</div>
|
||||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
||||||
|
|
||||||
<Drawer bind:this={drawer} title={"Actions"}>
|
<Drawer bind:this={drawer} title={"Actions"}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Define what actions to run.
|
Define what actions to run.
|
||||||
|
@ -85,3 +92,10 @@
|
||||||
{key}
|
{key}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.action-count {
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue