platform view casing
This commit is contained in:
commit
0db194b412
|
@ -162,6 +162,7 @@
|
||||||
"translation"
|
"translation"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
"login": "mslourens",
|
"login": "mslourens",
|
||||||
"name": "Maurits Lourens",
|
"name": "Maurits Lourens",
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4",
|
||||||
|
|
|
@ -69,6 +69,28 @@ jobs:
|
||||||
env:
|
env:
|
||||||
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
||||||
|
|
||||||
|
- name: Re roll app-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment app-service -n budibase
|
||||||
|
|
||||||
|
- name: Re roll proxy-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment proxy-service -n budibase
|
||||||
|
|
||||||
|
- name: Re roll worker-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment worker-service -n budibase
|
||||||
|
|
||||||
|
|
||||||
- name: Discord Webhook Action
|
- name: Discord Webhook Action
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -18,8 +18,9 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
# disable unless needed for testing
|
||||||
|
# POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
FEATURE_PREVIEW_URL: https://budirelease.live
|
FEATURE_PREVIEW_URL: https://budirelease.live
|
||||||
|
@ -119,6 +120,27 @@ jobs:
|
||||||
]
|
]
|
||||||
env:
|
env:
|
||||||
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
|
||||||
|
|
||||||
|
- name: Re roll app-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment app-service -n budibase
|
||||||
|
|
||||||
|
- name: Re roll proxy-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment proxy-service -n budibase
|
||||||
|
|
||||||
|
- name: Re roll worker-service
|
||||||
|
uses: actions-hub/kubectl@master
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: rollout restart deployment worker-service -n budibase
|
||||||
|
|
||||||
- name: Discord Webhook Action
|
- name: Discord Webhook Action
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: Budibase Smoke Test
|
name: Budibase Nightly Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -6,7 +6,7 @@ on:
|
||||||
- cron: "0 5 * * *" # every day at 5AM
|
- cron: "0 5 * * *" # every day at 5AM
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
nightly:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -43,6 +43,18 @@ jobs:
|
||||||
name: Test Reports
|
name: Test Reports
|
||||||
path: packages/builder/cypress/reports/testReport.html
|
path: packages/builder/cypress/reports/testReport.html
|
||||||
|
|
||||||
|
# TODO: enable once running in QA test env
|
||||||
|
# - name: Configure AWS Credentials
|
||||||
|
# uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
# with:
|
||||||
|
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
# aws-region: eu-west-1
|
||||||
|
|
||||||
|
# - name: Upload test results HTML
|
||||||
|
# uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
# run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html
|
||||||
|
|
||||||
- name: Cypress Discord Notify
|
- name: Cypress Discord Notify
|
||||||
run: yarn test:e2e:ci:notify
|
run: yarn test:e2e:ci:notify
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
"svelteSortOrder": "options-scripts-markup-styles"
|
"svelteSortOrder": "options-scripts-markup-styles"
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,10 @@ http {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /preview {
|
||||||
|
proxy_pass http://{{ address }}:4001;
|
||||||
|
}
|
||||||
|
|
||||||
location /builder {
|
location /builder {
|
||||||
proxy_pass http://{{ address }}:3000;
|
proxy_pass http://{{ address }}:3000;
|
||||||
rewrite ^/builder(.*)$ /builder/$1 break;
|
rewrite ^/builder(.*)$ /builder/$1 break;
|
||||||
|
|
|
@ -88,6 +88,10 @@ http {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /preview {
|
||||||
|
proxy_pass http://$apps:4002;
|
||||||
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +101,7 @@ http {
|
||||||
proxy_pass http://$watchtower:8080;
|
proxy_pass http://$watchtower:8080;
|
||||||
}
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
location ~ ^/(builder|app_) {
|
location ~ ^/(builder|app_) {
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
|
@ -3,15 +3,18 @@
|
||||||
echo ${TARGETBUILD} > /buildtarget.txt
|
echo ${TARGETBUILD} > /buildtarget.txt
|
||||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
# Azure AppService uses /home for persisent data & SSH on port 2222
|
# Azure AppService uses /home for persisent data & SSH on port 2222
|
||||||
mkdir -p /home/{search,minio,couch}
|
DATA_DIR=/home
|
||||||
mkdir -p /home/couch/{dbs,views}
|
mkdir -p $DATA_DIR/{search,minio,couchdb}
|
||||||
chown -R couchdb:couchdb /home/couch/
|
mkdir -p $DATA_DIR/couchdb/{dbs,views}
|
||||||
|
chown -R couchdb:couchdb $DATA_DIR/couchdb/
|
||||||
apt update
|
apt update
|
||||||
apt-get install -y openssh-server
|
apt-get install -y openssh-server
|
||||||
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/search#' /opt/clouseau/clouseau.ini
|
|
||||||
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/minio &#' /runner.sh
|
|
||||||
sed -i 's#database_dir = ./data#database_dir = /home/couch/dbs#' /opt/couchdb/etc/default.ini
|
|
||||||
sed -i 's#view_index_dir = ./data#view_index_dir = /home/couch/views#' /opt/couchdb/etc/default.ini
|
|
||||||
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
||||||
/etc/init.d/ssh restart
|
/etc/init.d/ssh restart
|
||||||
fi
|
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
|
||||||
|
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
|
||||||
|
else
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
|
||||||
|
fi
|
|
@ -20,10 +20,10 @@ RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
||||||
|
|
||||||
FROM couchdb:3.2.1
|
FROM couchdb:3.2.1
|
||||||
# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64
|
# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64
|
||||||
ARG TARGETARCH amd64
|
ARG TARGETARCH=amd64
|
||||||
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||||
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||||
ARG TARGETBUILD single
|
ARG TARGETBUILD=single
|
||||||
ENV TARGETBUILD $TARGETBUILD
|
ENV TARGETBUILD $TARGETBUILD
|
||||||
|
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
@ -35,6 +35,7 @@ ENV \
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION \
|
BUDIBASE_ENVIRONMENT=PRODUCTION \
|
||||||
CLUSTER_PORT=80 \
|
CLUSTER_PORT=80 \
|
||||||
# CUSTOM_DOMAIN=budi001.custom.com \
|
# CUSTOM_DOMAIN=budi001.custom.com \
|
||||||
|
DATA_DIR=/data \
|
||||||
DEPLOYMENT_ENVIRONMENT=docker \
|
DEPLOYMENT_ENVIRONMENT=docker \
|
||||||
MINIO_URL=http://localhost:9000 \
|
MINIO_URL=http://localhost:9000 \
|
||||||
POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \
|
POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \
|
||||||
|
@ -114,6 +115,7 @@ RUN chmod +x ./healthcheck.sh
|
||||||
ADD hosting/scripts/build-target-paths.sh .
|
ADD hosting/scripts/build-target-paths.sh .
|
||||||
RUN chmod +x ./build-target-paths.sh
|
RUN chmod +x ./build-target-paths.sh
|
||||||
|
|
||||||
|
# Script below sets the path for storing data based on $DATA_DIR
|
||||||
# For Azure App Service install SSH & point data locations to /home
|
# For Azure App Service install SSH & point data locations to /home
|
||||||
RUN /build-target-paths.sh
|
RUN /build-target-paths.sh
|
||||||
|
|
||||||
|
|
|
@ -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=/data/search
|
dir=DATA_DIR/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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
; CouchDB Configuration Settings
|
; CouchDB Configuration Settings
|
||||||
|
|
||||||
[couchdb]
|
[couchdb]
|
||||||
database_dir = /data/couch/dbs
|
database_dir = DATA_DIR/couchdb/dbs
|
||||||
view_index_dir = /data/couch/views
|
view_index_dir = DATA_DIR/couchdb/views
|
||||||
|
|
|
@ -3,6 +3,11 @@ healthy=true
|
||||||
|
|
||||||
if [ -f "/data/.env" ]; then
|
if [ -f "/data/.env" ]; then
|
||||||
export $(cat /data/.env | xargs)
|
export $(cat /data/.env | xargs)
|
||||||
|
elif [ -f "/home/.env" ]; then
|
||||||
|
export $(cat /home/.env | xargs)
|
||||||
|
else
|
||||||
|
echo "No .env file found"
|
||||||
|
healthy=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then
|
if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
||||||
if [ -f "/data/.env" ]; then
|
|
||||||
export $(cat /data/.env | xargs)
|
# Azure App Service customisations
|
||||||
|
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
|
DATA_DIR=/home
|
||||||
|
/etc/init.d/ssh start
|
||||||
|
else
|
||||||
|
DATA_DIR=${DATA_DIR:-/data}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${DATA_DIR}/.env" ]; then
|
||||||
|
export $(cat ${DATA_DIR}/.env | xargs)
|
||||||
fi
|
fi
|
||||||
# first randomise any unset environment variables
|
# first randomise any unset environment variables
|
||||||
for ENV_VAR in "${ENV_VARS[@]}"
|
for ENV_VAR in "${ENV_VARS[@]}"
|
||||||
|
@ -14,21 +23,26 @@ done
|
||||||
if [[ -z "${COUCH_DB_URL}" ]]; then
|
if [[ -z "${COUCH_DB_URL}" ]]; then
|
||||||
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||||
fi
|
fi
|
||||||
if [ ! -f "/data/.env" ]; then
|
if [ ! -f "${DATA_DIR}/.env" ]; then
|
||||||
touch /data/.env
|
touch ${DATA_DIR}/.env
|
||||||
for ENV_VAR in "${ENV_VARS[@]}"
|
for ENV_VAR in "${ENV_VARS[@]}"
|
||||||
do
|
do
|
||||||
temp=$(eval "echo \$$ENV_VAR")
|
temp=$(eval "echo \$$ENV_VAR")
|
||||||
echo "$ENV_VAR=$temp" >> /data/.env
|
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
|
||||||
done
|
done
|
||||||
|
echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||||
|
|
||||||
# make these directories in runner, incase of mount
|
# make these directories in runner, incase of mount
|
||||||
mkdir -p /data/couch/{dbs,views} /home/couch/{dbs,views}
|
mkdir -p ${DATA_DIR}/couchdb/{dbs,views}
|
||||||
chown -R couchdb:couchdb /data/couch /home/couch
|
mkdir -p ${DATA_DIR}/minio
|
||||||
|
mkdir -p ${DATA_DIR}/search
|
||||||
|
chown -R couchdb:couchdb ${DATA_DIR}/couchdb
|
||||||
redis-server --requirepass $REDIS_PASSWORD &
|
redis-server --requirepass $REDIS_PASSWORD &
|
||||||
/opt/clouseau/bin/clouseau &
|
/opt/clouseau/bin/clouseau &
|
||||||
/minio/minio server /data/minio &
|
/minio/minio server ${DATA_DIR}/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,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.2.58",
|
"version": "1.2.59",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.2.58",
|
"version": "1.2.59",
|
||||||
"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,13 +20,14 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "^1.2.58",
|
"@budibase/types": "^1.2.59",
|
||||||
"@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",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"emitter-listener": "1.1.2",
|
"emitter-listener": "1.1.2",
|
||||||
"ioredis": "4.28.0",
|
"ioredis": "4.28.0",
|
||||||
|
"joi": "17.6.0",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"koa-passport": "4.1.4",
|
"koa-passport": "4.1.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const passport = require("koa-passport")
|
const passport = require("koa-passport")
|
||||||
const LocalStrategy = require("passport-local").Strategy
|
const LocalStrategy = require("passport-local").Strategy
|
||||||
const JwtStrategy = require("passport-jwt").Strategy
|
const JwtStrategy = require("passport-jwt").Strategy
|
||||||
const { getGlobalDB } = require("./tenancy")
|
import { getGlobalDB } from "./tenancy"
|
||||||
const refresh = require("passport-oauth2-refresh")
|
const refresh = require("passport-oauth2-refresh")
|
||||||
const { Configs } = require("./constants")
|
import { Configs } from "./constants"
|
||||||
const { getScopedConfig } = require("./db/utils")
|
import { getScopedConfig } from "./db/utils"
|
||||||
const {
|
import {
|
||||||
jwt,
|
jwt,
|
||||||
local,
|
local,
|
||||||
authenticated,
|
authenticated,
|
||||||
|
@ -13,7 +13,6 @@ const {
|
||||||
oidc,
|
oidc,
|
||||||
auditLog,
|
auditLog,
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
|
||||||
authError,
|
authError,
|
||||||
ssoCallbackUrl,
|
ssoCallbackUrl,
|
||||||
csrf,
|
csrf,
|
||||||
|
@ -22,32 +21,36 @@ const {
|
||||||
builderOnly,
|
builderOnly,
|
||||||
builderOrAdmin,
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
} = require("./middleware")
|
} from "./middleware"
|
||||||
|
import { invalidateUser } from "./cache/user"
|
||||||
const { invalidateUser } = require("./cache/user")
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
passport.use(new LocalStrategy(local.options, local.authenticate))
|
passport.use(new LocalStrategy(local.options, local.authenticate))
|
||||||
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
|
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
|
||||||
|
|
||||||
passport.serializeUser((user, done) => done(null, user))
|
passport.serializeUser((user: User, done: any) => done(null, user))
|
||||||
|
|
||||||
passport.deserializeUser(async (user, done) => {
|
passport.deserializeUser(async (user: User, done: any) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await db.get(user._id)
|
const dbUser = await db.get(user._id)
|
||||||
return done(null, user)
|
return done(null, dbUser)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`User not found`, err)
|
console.error(`User not found`, err)
|
||||||
return done(null, false, { message: "User not found" })
|
return done(null, false, { message: "User not found" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
|
async function refreshOIDCAccessToken(
|
||||||
|
db: any,
|
||||||
|
chosenConfig: any,
|
||||||
|
refreshToken: string
|
||||||
|
) {
|
||||||
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
|
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
|
||||||
let enrichedConfig
|
let enrichedConfig: any
|
||||||
let strategy
|
let strategy: any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
|
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
|
||||||
|
@ -70,22 +73,28 @@ async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Configs.OIDC,
|
Configs.OIDC,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err, accessToken, refreshToken, params) => {
|
(err: any, accessToken: string, refreshToken: any, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshGoogleAccessToken(db, config, refreshToken) {
|
async function refreshGoogleAccessToken(
|
||||||
|
db: any,
|
||||||
|
config: any,
|
||||||
|
refreshToken: any
|
||||||
|
) {
|
||||||
let callbackUrl = await google.getCallbackUrl(db, config)
|
let callbackUrl = await google.getCallbackUrl(db, config)
|
||||||
|
|
||||||
let strategy
|
let strategy
|
||||||
try {
|
try {
|
||||||
strategy = await google.strategyFactory(config, callbackUrl)
|
strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw new Error("Error constructing OIDC refresh strategy", err)
|
throw new Error(
|
||||||
|
`Error constructing OIDC refresh strategy: message=${err.message}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh.use(strategy)
|
refresh.use(strategy)
|
||||||
|
@ -94,14 +103,18 @@ async function refreshGoogleAccessToken(db, config, refreshToken) {
|
||||||
refresh.requestNewAccessToken(
|
refresh.requestNewAccessToken(
|
||||||
Configs.GOOGLE,
|
Configs.GOOGLE,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
(err, accessToken, refreshToken, params) => {
|
(err: any, accessToken: string, refreshToken: string, params: any) => {
|
||||||
resolve({ err, accessToken, refreshToken, params })
|
resolve({ err, accessToken, refreshToken, params })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshOAuthToken(refreshToken, configType, configId) {
|
async function refreshOAuthToken(
|
||||||
|
refreshToken: string,
|
||||||
|
configType: string,
|
||||||
|
configId: string
|
||||||
|
) {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
const config = await getScopedConfig(db, {
|
const config = await getScopedConfig(db, {
|
||||||
|
@ -113,7 +126,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
|
||||||
let refreshResponse
|
let refreshResponse
|
||||||
if (configType === Configs.OIDC) {
|
if (configType === Configs.OIDC) {
|
||||||
// configId - retrieved from cookie.
|
// configId - retrieved from cookie.
|
||||||
chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
|
chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
||||||
if (!chosenConfig) {
|
if (!chosenConfig) {
|
||||||
throw new Error("Invalid OIDC configuration")
|
throw new Error("Invalid OIDC configuration")
|
||||||
}
|
}
|
||||||
|
@ -134,7 +147,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
|
||||||
return refreshResponse
|
return refreshResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserOAuth(userId, oAuthConfig) {
|
async function updateUserOAuth(userId: string, oAuthConfig: any) {
|
||||||
const details = {
|
const details = {
|
||||||
accessToken: oAuthConfig.accessToken,
|
accessToken: oAuthConfig.accessToken,
|
||||||
refreshToken: oAuthConfig.refreshToken,
|
refreshToken: oAuthConfig.refreshToken,
|
||||||
|
@ -162,14 +175,13 @@ async function updateUserOAuth(userId, oAuthConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export = {
|
||||||
buildAuthMiddleware: authenticated,
|
buildAuthMiddleware: authenticated,
|
||||||
passport,
|
passport,
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
jwt: require("jsonwebtoken"),
|
jwt: require("jsonwebtoken"),
|
||||||
buildTenancyMiddleware: tenancy,
|
buildTenancyMiddleware: tenancy,
|
||||||
buildAppTenancyMiddleware: appTenancy,
|
|
||||||
auditLog,
|
auditLog,
|
||||||
authError,
|
authError,
|
||||||
buildCsrfMiddleware: csrf,
|
buildCsrfMiddleware: csrf,
|
|
@ -18,6 +18,7 @@ export enum ViewName {
|
||||||
LINK = "by_link",
|
LINK = "by_link",
|
||||||
ROUTING = "screen_routes",
|
ROUTING = "screen_routes",
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
|
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ export enum DocumentType {
|
||||||
MIGRATIONS = "migrations",
|
MIGRATIONS = "migrations",
|
||||||
DEV_INFO = "devinfo",
|
DEV_INFO = "devinfo",
|
||||||
AUTOMATION_LOG = "log_au",
|
AUTOMATION_LOG = "log_au",
|
||||||
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
|
|
@ -233,6 +233,10 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) {
|
||||||
}
|
}
|
||||||
let dbs = await getAllDbs({ efficient })
|
let dbs = await getAllDbs({ efficient })
|
||||||
const appDbNames = dbs.filter((dbName: any) => {
|
const appDbNames = dbs.filter((dbName: any) => {
|
||||||
|
if (env.isTest() && !dbName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const split = dbName.split(SEPARATOR)
|
const split = dbName.split(SEPARATOR)
|
||||||
// it is an app, check the tenantId
|
// it is an app, check the tenantId
|
||||||
if (split[0] === DocumentType.APP) {
|
if (split[0] === DocumentType.APP) {
|
||||||
|
|
|
@ -5,6 +5,8 @@ const {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
} = require("./utils")
|
} = require("./utils")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
const { StaticDatabases } = require("./constants")
|
||||||
|
const { doWithDB } = require("./")
|
||||||
|
|
||||||
const DESIGN_DB = "_design/database"
|
const DESIGN_DB = "_design/database"
|
||||||
|
|
||||||
|
@ -56,6 +58,31 @@ exports.createNewUserEmailView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createAccountEmailView = async () => {
|
||||||
|
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get(DESIGN_DB)
|
||||||
|
} catch (err) {
|
||||||
|
// no design doc, make one
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
// if using variables in a map function need to inject them before use
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
|
||||||
|
emit(doc.email.toLowerCase(), doc._id)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewName.ACCOUNT_BY_EMAIL]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
exports.createUserAppView = async () => {
|
exports.createUserAppView = async () => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
|
@ -128,24 +155,27 @@ exports.createUserBuildersView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createPlatformUserView = async db => {
|
exports.createPlatformUserView = async () => {
|
||||||
let designDoc
|
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
try {
|
let designDoc
|
||||||
designDoc = await db.get(DESIGN_DB)
|
try {
|
||||||
} catch (err) {
|
designDoc = await db.get(DESIGN_DB)
|
||||||
// no design doc, make one
|
} catch (err) {
|
||||||
designDoc = DesignDoc()
|
// no design doc, make one
|
||||||
}
|
designDoc = DesignDoc()
|
||||||
const view = {
|
}
|
||||||
map: `function(doc) {
|
const view = {
|
||||||
emit(doc._id.toLowerCase(), doc._id)
|
// if using variables in a map function need to inject them before use
|
||||||
}`,
|
map: `function(doc) {
|
||||||
}
|
emit(doc._id.toLowerCase(), doc._id)
|
||||||
designDoc.views = {
|
}`,
|
||||||
...designDoc.views,
|
}
|
||||||
[ViewName.PLATFORM_USERS_LOWERCASE]: view,
|
designDoc.views = {
|
||||||
}
|
...designDoc.views,
|
||||||
await db.put(designDoc)
|
[ViewName.PLATFORM_USERS_LOWERCASE]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
|
@ -154,7 +184,6 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
[ViewName.BY_API_KEY]: exports.createApiKeyView,
|
[ViewName.BY_API_KEY]: exports.createApiKeyView,
|
||||||
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
[ViewName.USER_BY_APP]: exports.createUserAppView,
|
[ViewName.USER_BY_APP]: exports.createUserAppView,
|
||||||
[ViewName.PLATFORM_USERS_LOWERCASE]: exports.createPlatformUserView,
|
|
||||||
}
|
}
|
||||||
// can pass DB in if working with something specific
|
// can pass DB in if working with something specific
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
@ -165,15 +194,67 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
response = response.map(resp =>
|
response = response.map(resp =>
|
||||||
params.include_docs ? resp.doc : resp.value
|
params.include_docs ? resp.doc : resp.value
|
||||||
)
|
)
|
||||||
return response.length <= 1 ? response[0] : response
|
if (params.arrayResponse) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
return response.length <= 1 ? response[0] : response
|
||||||
|
}
|
||||||
} 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 removeDeprecated(db, viewName)
|
||||||
await createFunc(db)
|
await createFunc(db)
|
||||||
return exports.queryGlobalView(viewName, params)
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.queryView = async (viewName, params, db, CreateFuncByName) => {
|
||||||
|
try {
|
||||||
|
let response = (await db.query(`database/${viewName}`, params)).rows
|
||||||
|
response = response.map(resp =>
|
||||||
|
params.include_docs ? resp.doc : resp.value
|
||||||
|
)
|
||||||
|
if (params.arrayResponse) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
return response.length <= 1 ? response[0] : response
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.name === "not_found") {
|
||||||
|
const createFunc = CreateFuncByName[viewName]
|
||||||
|
await removeDeprecated(db, viewName)
|
||||||
|
await createFunc()
|
||||||
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.queryPlatformView = async (viewName, params) => {
|
||||||
|
const CreateFuncByName = {
|
||||||
|
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
|
||||||
|
[ViewName.PLATFORM_USERS_LOWERCASE]: exports.createPlatformUserView,
|
||||||
|
}
|
||||||
|
|
||||||
|
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
|
const CreateFuncByName = {
|
||||||
|
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||||
|
[ViewName.BY_API_KEY]: exports.createApiKeyView,
|
||||||
|
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
|
[ViewName.USER_BY_APP]: exports.createUserAppView,
|
||||||
|
}
|
||||||
|
// can pass DB in if working with something specific
|
||||||
|
if (!db) {
|
||||||
|
db = getGlobalDB()
|
||||||
|
}
|
||||||
|
return exports.queryView(viewName, params, db, CreateFuncByName)
|
||||||
|
}
|
||||||
|
|
|
@ -8,4 +8,5 @@ import { processors } from "./processors"
|
||||||
|
|
||||||
export const shutdown = () => {
|
export const shutdown = () => {
|
||||||
processors.shutdown()
|
processors.shutdown()
|
||||||
|
console.log("Events shutdown")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
import logging from "./logging"
|
import logging from "./logging"
|
||||||
import pino from "./pino"
|
import pino from "./pino"
|
||||||
|
import * as middleware from "./middleware"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -57,6 +58,7 @@ const core = {
|
||||||
roles,
|
roles,
|
||||||
...pino,
|
...pino,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
|
middleware,
|
||||||
}
|
}
|
||||||
|
|
||||||
export = core
|
export = core
|
||||||
|
|
|
@ -65,7 +65,7 @@ async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||||
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
||||||
* has not yet been populated.
|
* has not yet been populated.
|
||||||
*/
|
*/
|
||||||
module.exports = (
|
export = (
|
||||||
noAuthPatterns = [],
|
noAuthPatterns = [],
|
||||||
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
||||||
publicAllowed: false,
|
publicAllowed: false,
|
||||||
|
|
|
@ -13,7 +13,8 @@ const adminOnly = require("./adminOnly")
|
||||||
const builderOrAdmin = require("./builderOrAdmin")
|
const builderOrAdmin = require("./builderOrAdmin")
|
||||||
const builderOnly = require("./builderOnly")
|
const builderOnly = require("./builderOnly")
|
||||||
const joiValidator = require("./joi-validator")
|
const joiValidator = require("./joi-validator")
|
||||||
module.exports = {
|
|
||||||
|
const pkg = {
|
||||||
google,
|
google,
|
||||||
oidc,
|
oidc,
|
||||||
jwt,
|
jwt,
|
||||||
|
@ -33,3 +34,5 @@ module.exports = {
|
||||||
builderOrAdmin,
|
builderOrAdmin,
|
||||||
joiValidator,
|
joiValidator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -1,3 +1,5 @@
|
||||||
|
const Joi = require("joi")
|
||||||
|
|
||||||
function validate(schema, property) {
|
function validate(schema, property) {
|
||||||
// Return a Koa middleware function
|
// Return a Koa middleware function
|
||||||
return (ctx, next) => {
|
return (ctx, next) => {
|
||||||
|
@ -10,6 +12,15 @@ function validate(schema, property) {
|
||||||
} else if (ctx.request[property] != null) {
|
} else if (ctx.request[property] != null) {
|
||||||
params = ctx.request[property]
|
params = ctx.request[property]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not all schemas have the append property e.g. array schemas
|
||||||
|
if (schema.append) {
|
||||||
|
schema = schema.append({
|
||||||
|
createdAt: Joi.any().optional(),
|
||||||
|
updatedAt: Joi.any().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params)
|
||||||
if (error) {
|
if (error) {
|
||||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||||
|
|
|
@ -3,17 +3,27 @@ const { v4: uuidv4 } = require("uuid")
|
||||||
const { logWarn } = require("../logging")
|
const { logWarn } = require("../logging")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
interface Session {
|
interface CreateSession {
|
||||||
key: string
|
|
||||||
userId: string
|
|
||||||
sessionId: string
|
sessionId: string
|
||||||
lastAccessedAt: string
|
tenantId: string
|
||||||
createdAt: string
|
|
||||||
csrfToken?: string
|
csrfToken?: string
|
||||||
value: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionKey = { key: string }[]
|
interface Session extends CreateSession {
|
||||||
|
userId: string
|
||||||
|
lastAccessedAt: string
|
||||||
|
createdAt: string
|
||||||
|
// make optional attributes required
|
||||||
|
csrfToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionKey {
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScannedSession {
|
||||||
|
value: Session
|
||||||
|
}
|
||||||
|
|
||||||
// a week in seconds
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
@ -22,14 +32,14 @@ function makeSessionID(userId: string, sessionId: string) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessionsForUser(userId: string) {
|
export async function getSessionsForUser(userId: string): Promise<Session[]> {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.trace("Cannot get sessions for undefined userId")
|
console.trace("Cannot get sessions for undefined userId")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessions = await client.scan(userId)
|
const sessions: ScannedSession[] = await client.scan(userId)
|
||||||
return sessions.map((session: Session) => session.value)
|
return sessions.map(session => session.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invalidateSessions(
|
export async function invalidateSessions(
|
||||||
|
@ -39,33 +49,32 @@ export async function invalidateSessions(
|
||||||
try {
|
try {
|
||||||
const reason = opts?.reason || "unknown"
|
const reason = opts?.reason || "unknown"
|
||||||
let sessionIds: string[] = opts.sessionIds || []
|
let sessionIds: string[] = opts.sessionIds || []
|
||||||
let sessions: SessionKey
|
let sessionKeys: SessionKey[]
|
||||||
|
|
||||||
// If no sessionIds, get all the sessions for the user
|
// If no sessionIds, get all the sessions for the user
|
||||||
if (sessionIds.length === 0) {
|
if (sessionIds.length === 0) {
|
||||||
sessions = await getSessionsForUser(userId)
|
const sessions = await getSessionsForUser(userId)
|
||||||
sessions.forEach(
|
sessionKeys = sessions.map(session => ({
|
||||||
(session: any) =>
|
key: makeSessionID(session.userId, session.sessionId),
|
||||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
}))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// use the passed array of sessionIds
|
// use the passed array of sessionIds
|
||||||
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||||
sessions = sessionIds.map((sessionId: string) => ({
|
sessionKeys = sessionIds.map(sessionId => ({
|
||||||
key: makeSessionID(userId, sessionId),
|
key: makeSessionID(userId, sessionId),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessions && sessions.length > 0) {
|
if (sessionKeys && sessionKeys.length > 0) {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const promises = []
|
const promises = []
|
||||||
for (let session of sessions) {
|
for (let sessionKey of sessionKeys) {
|
||||||
promises.push(client.delete(session.key))
|
promises.push(client.delete(sessionKey.key))
|
||||||
}
|
}
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
logWarn(
|
logWarn(
|
||||||
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
|
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys
|
||||||
.map(session => session.key)
|
.map(sessionKey => sessionKey.key)
|
||||||
.join(", ")}`
|
.join(", ")}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -76,22 +85,26 @@ export async function invalidateSessions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createASession(userId: string, session: Session) {
|
export async function createASession(
|
||||||
|
userId: string,
|
||||||
|
createSession: CreateSession
|
||||||
|
) {
|
||||||
// invalidate all other sessions
|
// invalidate all other sessions
|
||||||
await invalidateSessions(userId, { reason: "creation" })
|
await invalidateSessions(userId, { reason: "creation" })
|
||||||
|
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessionId = session.sessionId
|
const sessionId = createSession.sessionId
|
||||||
if (!session.csrfToken) {
|
const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4()
|
||||||
session.csrfToken = uuidv4()
|
const key = makeSessionID(userId, sessionId)
|
||||||
}
|
|
||||||
session = {
|
const session: Session = {
|
||||||
...session,
|
...createSession,
|
||||||
|
csrfToken,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastAccessedAt: new Date().toISOString(),
|
lastAccessedAt: new Date().toISOString(),
|
||||||
userId,
|
userId,
|
||||||
}
|
}
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
await client.store(key, session, EXPIRY_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSessionTTL(session: Session) {
|
export async function updateSessionTTL(session: Session) {
|
||||||
|
@ -106,7 +119,10 @@ export async function endSession(userId: string, sessionId: string) {
|
||||||
await client.delete(makeSessionID(userId, sessionId))
|
await client.delete(makeSessionID(userId, sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSession(userId: string, sessionId: string) {
|
export async function getSession(
|
||||||
|
userId: string,
|
||||||
|
sessionId: string
|
||||||
|
): Promise<Session> {
|
||||||
if (!userId || !sessionId) {
|
if (!userId || !sessionId) {
|
||||||
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
|
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ const { UNICODE_MAX } = require("./db/constants")
|
||||||
* Given an email address this will use a view to search through
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
* @param {string} email the email to lookup the user by.
|
* @param {string} email the email to lookup the user by.
|
||||||
* @return {Promise<object|null>}
|
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const getAccount = jest.fn()
|
||||||
|
export const getAccountByTenantId = jest.fn()
|
||||||
|
|
||||||
|
jest.mock("../../../src/cloud/accounts", () => ({
|
||||||
|
getAccount,
|
||||||
|
getAccountByTenantId,
|
||||||
|
}))
|
|
@ -1,2 +0,0 @@
|
||||||
exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
|
|
||||||
exports.MOCK_DATE_TIMESTAMP = 1577836800000
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
|
||||||
|
export const MOCK_DATE_TIMESTAMP = 1577836800000
|
|
@ -1,9 +0,0 @@
|
||||||
const posthog = require("./posthog")
|
|
||||||
const events = require("./events")
|
|
||||||
const date = require("./date")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
posthog,
|
|
||||||
date,
|
|
||||||
events,
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import "./posthog"
|
||||||
|
import "./events"
|
||||||
|
export * as accounts from "./accounts"
|
||||||
|
export * as date from "./date"
|
|
@ -291,6 +291,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
|
"@hapi/hoek@^9.0.0":
|
||||||
|
version "9.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||||
|
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
|
||||||
|
|
||||||
|
"@hapi/topo@^5.0.0":
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
|
||||||
|
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||||
|
@ -539,6 +551,23 @@
|
||||||
koa "^2.13.4"
|
koa "^2.13.4"
|
||||||
node-mocks-http "^1.5.8"
|
node-mocks-http "^1.5.8"
|
||||||
|
|
||||||
|
"@sideway/address@^4.1.3":
|
||||||
|
version "4.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
|
||||||
|
integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
|
"@sideway/formula@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||||
|
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||||
|
|
||||||
|
"@sideway/pinpoint@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||||
|
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||||
|
|
||||||
"@sindresorhus/is@^0.14.0":
|
"@sindresorhus/is@^0.14.0":
|
||||||
version "0.14.0"
|
version "0.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
|
@ -3193,6 +3222,17 @@ jmespath@0.15.0:
|
||||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||||
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
|
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
|
||||||
|
|
||||||
|
joi@17.6.0:
|
||||||
|
version "17.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
|
||||||
|
integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
"@hapi/topo" "^5.0.0"
|
||||||
|
"@sideway/address" "^4.1.3"
|
||||||
|
"@sideway/formula" "^3.0.0"
|
||||||
|
"@sideway/pinpoint" "^2.0.0"
|
||||||
|
|
||||||
join-component@^1.1.0:
|
join-component@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||||
|
|
|
@ -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.2.58",
|
"version": "1.2.59",
|
||||||
"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.2.58",
|
"@budibase/string-templates": "^1.2.59",
|
||||||
"@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",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
export let timeOnly = false
|
export let timeOnly = false
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
|
export let time24hr = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
enableTime: timeOnly || enableTime || false,
|
enableTime: timeOnly || enableTime || false,
|
||||||
noCalendar: timeOnly || false,
|
noCalendar: timeOnly || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
|
time_24hr: time24hr || false,
|
||||||
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
|
@ -49,6 +51,12 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: redrawOptions = {
|
||||||
|
timeOnly,
|
||||||
|
enableTime,
|
||||||
|
time24hr,
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
const [dates] = event.detail
|
const [dates] = event.detail
|
||||||
const noTimezone = enableTime && !timeOnly && ignoreTimezones
|
const noTimezone = enableTime && !timeOnly && ignoreTimezones
|
||||||
|
@ -149,7 +157,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key timeOnly}
|
{#key redrawOptions}
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
bind:flatpickr
|
bind:flatpickr
|
||||||
value={parseDate(value)}
|
value={parseDate(value)}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let fileSizeLimit = BYTES_IN_MB * 20
|
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||||
export let processFiles = null
|
export let processFiles = null
|
||||||
|
export let deleteAttachments = null
|
||||||
export let handleFileTooLarge = null
|
export let handleFileTooLarge = null
|
||||||
export let handleTooManyFiles = null
|
export let handleTooManyFiles = null
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
|
@ -94,6 +95,11 @@
|
||||||
"change",
|
"change",
|
||||||
value.filter((x, idx) => idx !== selectedImageIdx)
|
value.filter((x, idx) => idx !== selectedImageIdx)
|
||||||
)
|
)
|
||||||
|
if (deleteAttachments) {
|
||||||
|
await deleteAttachments(
|
||||||
|
value.filter((x, idx) => idx === selectedImageIdx).map(item => item.key)
|
||||||
|
)
|
||||||
|
}
|
||||||
selectedImageIdx = 0
|
selectedImageIdx = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
$: toggleOption = makeToggleOption(selectedLookupMap, value)
|
$: toggleOption = makeToggleOption(selectedLookupMap, value)
|
||||||
|
|
||||||
const getFieldText = (value, map, placeholder) => {
|
const getFieldText = (value, map, placeholder) => {
|
||||||
if (value?.length) {
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
const getSelectedLookupMap = value => {
|
const getSelectedLookupMap = value => {
|
||||||
let map = {}
|
let map = {}
|
||||||
if (value?.length) {
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
value.forEach(option => {
|
value.forEach(option => {
|
||||||
if (option) {
|
if (option) {
|
||||||
map[option] = true
|
map[option] = true
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = option => option
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
|
export let getOptionTitle = option => option
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => dispatch("change", e.target.value)
|
const onChange = e => dispatch("change", e.target.value)
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
{#if options && Array.isArray(options)}
|
{#if options && Array.isArray(options)}
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
<div
|
<div
|
||||||
title={getOptionLabel(option)}
|
title={getOptionTitle(option)}
|
||||||
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
|
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
>
|
>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let error = null
|
export let error = null
|
||||||
export let enableTime = true
|
export let enableTime = true
|
||||||
export let timeOnly = false
|
export let timeOnly = false
|
||||||
|
export let time24hr = false
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{enableTime}
|
{enableTime}
|
||||||
{timeOnly}
|
{timeOnly}
|
||||||
|
{time24hr}
|
||||||
{appendTo}
|
{appendTo}
|
||||||
{ignoreTimezones}
|
{ignoreTimezones}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let error = null
|
export let error = null
|
||||||
export let fileSizeLimit = undefined
|
export let fileSizeLimit = undefined
|
||||||
export let processFiles = undefined
|
export let processFiles = undefined
|
||||||
|
export let deleteAttachments = undefined
|
||||||
export let handleFileTooLarge = undefined
|
export let handleFileTooLarge = undefined
|
||||||
export let handleTooManyFiles = undefined
|
export let handleTooManyFiles = undefined
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
{value}
|
{value}
|
||||||
{fileSizeLimit}
|
{fileSizeLimit}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
|
{deleteAttachments}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{gallery}
|
{gallery}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let direction = "vertical"
|
export let direction = "vertical"
|
||||||
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 getOptionTitle = option => extractProperty(option, "label")
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
{direction}
|
{direction}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
|
{getOptionTitle}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -83,4 +83,9 @@
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-Icon--sizeXS {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const actionMenu = getContext("actionMenu")
|
const actionMenu = getContext("actionMenu")
|
||||||
|
@ -8,6 +9,22 @@
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let disabled = undefined
|
export let disabled = undefined
|
||||||
export let noClose = false
|
export let noClose = false
|
||||||
|
export let keyBind = undefined
|
||||||
|
|
||||||
|
$: keys = getKeys(keyBind)
|
||||||
|
|
||||||
|
const getKeys = keyBind => {
|
||||||
|
let keys = keyBind?.split("+") || []
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (
|
||||||
|
keys[i].toLowerCase() === "ctrl" &&
|
||||||
|
navigator.platform.startsWith("Mac")
|
||||||
|
) {
|
||||||
|
keys[i] = "⌘"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (actionMenu && !noClose) {
|
if (actionMenu && !noClose) {
|
||||||
|
@ -26,20 +43,54 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<div class="icon">
|
||||||
class="spectrum-Icon spectrum-Icon--sizeS spectrum-Menu-itemIcon"
|
<Icon name={icon} size="S" />
|
||||||
focusable="false"
|
</div>
|
||||||
aria-hidden="true"
|
|
||||||
aria-label={icon}
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
||||||
|
{#if keys?.length}
|
||||||
|
<div class="keys">
|
||||||
|
{#each keys as key}
|
||||||
|
<div class="key">
|
||||||
|
{#if key.startsWith("!")}
|
||||||
|
<Icon size="XS" name={key.split("!")[1]} />
|
||||||
|
{:else}
|
||||||
|
{key}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Menu-itemIcon {
|
.icon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.keys {
|
||||||
|
margin-left: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.key {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 12px;
|
||||||
|
height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
margin: -1px 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.is-disabled .key {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = fixed || inline
|
let visible = fixed || inline
|
||||||
|
let modal
|
||||||
|
|
||||||
$: dispatch(visible ? "show" : "hide")
|
$: dispatch(visible ? "show" : "hide")
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
|
@ -41,12 +43,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function focusFirstInput(node) {
|
async function focusModal(node) {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
// Try to focus first input
|
||||||
const inputs = node.querySelectorAll("input")
|
const inputs = node.querySelectorAll("input")
|
||||||
if (inputs?.length) {
|
if (inputs?.length) {
|
||||||
await tick()
|
|
||||||
inputs[0].focus()
|
inputs[0].focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise try to focus confirmation button
|
||||||
|
else if (modal) {
|
||||||
|
const confirm = modal.querySelector(".confirm-wrap .spectrum-Button")
|
||||||
|
if (confirm) {
|
||||||
|
confirm.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(Context.Modal, { show, hide, cancel })
|
setContext(Context.Modal, { show, hide, cancel })
|
||||||
|
@ -56,7 +68,7 @@
|
||||||
|
|
||||||
{#if inline}
|
{#if inline}
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
<div use:focusModal bind:this={modal} class="spectrum-Modal inline is-open">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -70,17 +82,18 @@
|
||||||
-->
|
-->
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div
|
<div class="spectrum-Underlay is-open" on:mousedown|self={cancel}>
|
||||||
class="spectrum-Underlay is-open"
|
<div
|
||||||
in:fade={{ duration: 200 }}
|
class="background"
|
||||||
out:fade|local={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
on:mousedown|self={cancel}
|
out:fade|local={{ duration: 200 }}
|
||||||
>
|
/>
|
||||||
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
||||||
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
||||||
<slot name="outside" />
|
<slot name="outside" />
|
||||||
<div
|
<div
|
||||||
use:focusFirstInput
|
use:focusModal
|
||||||
|
bind:this={modal}
|
||||||
class="spectrum-Modal is-open"
|
class="spectrum-Modal is-open"
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
in:fly={{ y: 30, duration: 200 }}
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
out:fly|local={{ y: 30, duration: 200 }}
|
||||||
|
@ -103,7 +116,17 @@
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: rgba(0, 0, 0, 0.75);
|
background: transparent;
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
background: var(--modal-background, rgba(0, 0, 0, 0.75));
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
opacity: 0.65;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
}
|
}
|
||||||
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||||
margin-top: var(--spacing-xs);
|
margin-top: var(--spacing-xs);
|
||||||
|
|
|
@ -23,7 +23,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||||
|
@ -41,10 +41,25 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
|
||||||
}
|
}
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
||||||
|
//cy.logoutNoAppGrid()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should verify Standard Portal", () => {
|
||||||
|
// Development access should be disabled (Admin access is already disabled)
|
||||||
|
cy.login()
|
||||||
|
cy.setUserRole("bbuser", "App User")
|
||||||
|
bbUserLogin()
|
||||||
|
|
||||||
|
// Verify Standard Portal
|
||||||
|
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
|
||||||
|
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
|
||||||
|
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
|
||||||
|
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
|
||||||
|
|
||||||
cy.logoutNoAppGrid()
|
cy.logoutNoAppGrid()
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("should verify Admin Portal", () => {
|
it("should verify Admin Portal", () => {
|
||||||
cy.login()
|
cy.login()
|
||||||
// Configure user role
|
// Configure user role
|
||||||
cy.setUserRole("bbuser", "Admin")
|
cy.setUserRole("bbuser", "Admin")
|
||||||
|
@ -86,21 +101,6 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.logOut()
|
cy.logOut()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should verify Standard Portal", () => {
|
|
||||||
// Development access should be disabled (Admin access is already disabled)
|
|
||||||
cy.login()
|
|
||||||
cy.setUserRole("bbuser", "App User")
|
|
||||||
bbUserLogin()
|
|
||||||
|
|
||||||
// Verify Standard Portal
|
|
||||||
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
|
|
||||||
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
|
|
||||||
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
|
|
||||||
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
|
|
||||||
|
|
||||||
cy.logoutNoAppGrid()
|
|
||||||
})
|
|
||||||
|
|
||||||
const bbUserLogin = () => {
|
const bbUserLogin = () => {
|
||||||
// Login as bbuser
|
// Login as bbuser
|
||||||
cy.logOut()
|
cy.logOut()
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
// const interact = require("../support/interact")
|
||||||
|
|
||||||
|
filterTests(["smoke", "all"], () => {
|
||||||
|
context("Auth Configuration", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
cy.location().should(loc => {
|
||||||
|
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get("[data-cy=new-scope-input]").clear()
|
||||||
|
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
cy.get("[data-cy=oidc-active]").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=oidc-active]").should('not.be.checked')
|
||||||
|
|
||||||
|
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||||
|
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||||
|
cy.wait("@updateAuth")
|
||||||
|
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Toast-content")
|
||||||
|
.contains("Settings saved")
|
||||||
|
.should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow updating of the OIDC config", () => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
cy.location().should(loc => {
|
||||||
|
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||||
|
})
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
cy.get(".spectrum-Toast .spectrum-ClearButton").click()
|
||||||
|
|
||||||
|
cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2")
|
||||||
|
cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2")
|
||||||
|
cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_")
|
||||||
|
|
||||||
|
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||||
|
|
||||||
|
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||||
|
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||||
|
cy.wait("@updateAuth")
|
||||||
|
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Toast-content")
|
||||||
|
.contains("Settings saved")
|
||||||
|
.should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should display default scopes in advanced config.", () => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
cy.location().should(loc => {
|
||||||
|
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||||
|
})
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("openid")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("email")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Add a new scopes", () => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
cy.location().should(loc => {
|
||||||
|
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||||
|
})
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
|
||||||
|
cy.get("[data-cy=new-scope-input]").type("Sample{enter}")
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5)
|
||||||
|
cy.get(".spectrum-Tags-item").contains("Sample")
|
||||||
|
|
||||||
|
cy.get(".auth-form input.spectrum-Textfield-input").type("Another ")
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6)
|
||||||
|
cy.get(".spectrum-Tags-item").contains("Another")
|
||||||
|
|
||||||
|
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||||
|
|
||||||
|
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||||
|
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||||
|
cy.wait("@updateAuth")
|
||||||
|
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||||
|
|
||||||
|
cy.reload()
|
||||||
|
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("openid")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("email")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("Sample")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("Another")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow the removal of auth scopes", () => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
cy.location().should(loc => {
|
||||||
|
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||||
|
})
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click()
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
|
||||||
|
|
||||||
|
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||||
|
|
||||||
|
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||||
|
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||||
|
cy.wait("@updateAuth")
|
||||||
|
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Toast-content")
|
||||||
|
.contains("Settings saved")
|
||||||
|
.should("be.visible")
|
||||||
|
|
||||||
|
cy.reload()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow auth scopes to be reset to the core defaults.", () => {
|
||||||
|
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||||
|
|
||||||
|
cy.get("div.content").scrollTo("bottom")
|
||||||
|
|
||||||
|
cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tags-item").contains("openid")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("email")
|
||||||
|
cy.get(".spectrum-Tags-item").contains("profile")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should not allow invalid characters in the auth scopes", () => {
|
||||||
|
cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}")
|
||||||
|
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get("[data-cy=new-scope-input]").clear()
|
||||||
|
|
||||||
|
cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}")
|
||||||
|
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
|
||||||
|
cy.get("[data-cy=new-scope-input]").clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should not allow duplicate auth scopes", () => {
|
||||||
|
cy.get("[data-cy=new-scope-input]").type("offline_access{enter}")
|
||||||
|
cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists")
|
||||||
|
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -17,7 +17,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
|
|
||||||
it("should confirm App User role for a New User", () => {
|
it("should confirm App User role for a New User", () => {
|
||||||
cy.contains("bbuser").click()
|
cy.contains("bbuser").click()
|
||||||
cy.get(".spectrum-Form-itemField").eq(2).should('contain', 'App User')
|
cy.get(".spectrum-Form-itemField").eq(3).should('contain', 'App User')
|
||||||
|
|
||||||
// User should not have app access
|
// User should not have app access
|
||||||
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps")
|
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps")
|
||||||
|
@ -166,12 +166,12 @@ filterTests(["smoke", "all"], () => {
|
||||||
|
|
||||||
it("Should edit user details within user details page", () => {
|
it("Should edit user details within user details page", () => {
|
||||||
// Add First name
|
// Add First name
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
||||||
})
|
})
|
||||||
// Add Last name
|
// Add Last name
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
||||||
})
|
})
|
||||||
|
@ -180,10 +180,10 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.reload()
|
cy.reload()
|
||||||
|
|
||||||
// Confirm details have been saved
|
// Confirm details have been saved
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
||||||
})
|
})
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -193,13 +193,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset password modal
|
// Reset password modal
|
||||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
||||||
|
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").should('not.exist')
|
||||||
|
|
||||||
// Logout, then login with new password
|
// Logout, then login with new password
|
||||||
cy.logOut()
|
cy.logOut()
|
||||||
|
@ -214,6 +215,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
||||||
|
|
||||||
// Confirm user logged in afer password change
|
// Confirm user logged in afer password change
|
||||||
|
cy.login("bbuser@test.com", "test")
|
||||||
cy.get(".avatar > .icon").click({ force: true })
|
cy.get(".avatar > .icon").click({ force: true })
|
||||||
|
|
||||||
cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true })
|
cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true })
|
||||||
|
|
|
@ -19,10 +19,10 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
cy.contains("test@test.com").click()
|
cy.contains("test@test.com").click()
|
||||||
|
|
||||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
||||||
})
|
})
|
||||||
cy.get(interact.FIELD).eq(1).within(() => {
|
cy.get(interact.FIELD).eq(2).within(() => {
|
||||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -72,7 +72,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Logout & in with new password
|
// Logout & in with new password
|
||||||
cy.logOut()
|
//cy.logOut()
|
||||||
cy.login("test@test.com", "newpwd")
|
cy.login("test@test.com", "newpwd")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -90,7 +90,6 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true })
|
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true })
|
||||||
cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available
|
cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available
|
||||||
cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available
|
cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available
|
||||||
cy.get(interact.APP_TABLE).should('exist') // App table available
|
|
||||||
})
|
})
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
|
|
|
@ -266,7 +266,7 @@ filterTests(["all"], () => {
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.log("Current deployment version: " + clientPackage.version)
|
cy.log("Current deployment version: " + clientPackage.version)
|
||||||
|
|
||||||
cy.get(".version-status a", { timeout: 1000 }).contains("Update").click()
|
cy.get(".version-status a", { timeout: 5000 }).contains("Update").click()
|
||||||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
|
||||||
cy.get(".version-section .page-action button")
|
cy.get(".version-section .page-action button")
|
||||||
|
|
|
@ -102,7 +102,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 })
|
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished")
|
cy.get(interact.APP_TABLE_STATUS, { timeout: 10000 }).eq(0).contains("Unpublished")
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -94,6 +94,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create the first application from scratch with a default name", () => {
|
it("should create the first application from scratch with a default name", () => {
|
||||||
|
cy.updateUserInformation("", "")
|
||||||
cy.createApp("", false)
|
cy.createApp("", false)
|
||||||
cy.applicationInAppTable("My app")
|
cy.applicationInAppTable("My app")
|
||||||
cy.deleteApp("My app")
|
cy.deleteApp("My app")
|
||||||
|
|
|
@ -48,7 +48,7 @@ filterTests(["smoke", "all"], () => {
|
||||||
|
|
||||||
it("deletes a row", () => {
|
it("deletes a row", () => {
|
||||||
cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true })
|
cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true })
|
||||||
cy.contains("Delete 1 row(s)").click()
|
cy.contains("Delete 1 row").click()
|
||||||
cy.get(interact.SPECTRUM_MODAL).contains("Delete").click()
|
cy.get(interact.SPECTRUM_MODAL).contains("Delete").click()
|
||||||
cy.contains("RoverUpdated").should("not.exist")
|
cy.contains("RoverUpdated").should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
|
@ -175,7 +175,10 @@ filterTests(["all"], () => {
|
||||||
cy.get("@query").its("response.statusCode").should("eq", 200)
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
cy.get("@query").its("response.body").should("not.be.empty")
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
// Save query
|
// Save query
|
||||||
|
cy.intercept("POST", "**/queries").as("saveQuery")
|
||||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.wait("@saveQuery")
|
||||||
|
cy.get("@saveQuery").its("response.statusCode").should("eq", 200)
|
||||||
cy.get(".nav-item").should("contain", queryName)
|
cy.get(".nav-item").should("contain", queryName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -252,7 +252,8 @@ filterTests(["all"], () => {
|
||||||
.contains("Delete Query")
|
.contains("Delete Query")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
cy.reload({ timeout: 5000 })
|
cy.reload()
|
||||||
|
cy.get(".nav-item", { timeout: 30000 }).contains(datasource).click({ force: true })
|
||||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(interact.AREA_LABEL_REVERT).click({ force: true })
|
cy.get(interact.AREA_LABEL_REVERT).click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => {
|
cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => {
|
||||||
|
cy.get("input").type("Cypress Tests")
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
|
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
|
||||||
cy.wait(2000) // Wait for app to finish reverting
|
cy.wait(2000) // Wait for app to finish reverting
|
||||||
|
|
|
@ -128,7 +128,9 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||||
.should("have.value", lastName)
|
.should("have.value", lastName)
|
||||||
.blur()
|
.blur()
|
||||||
}
|
}
|
||||||
cy.get("button").contains("Update information").click({ force: true })
|
cy.get(".confirm-wrap").within(() => {
|
||||||
|
cy.get("button").contains("Update information").click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").should("not.exist")
|
cy.get(".spectrum-Dialog-grid").should("not.exist")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -140,14 +142,14 @@ Cypress.Commands.add("setUserRole", (user, role) => {
|
||||||
// Set Role
|
// Set Role
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(".spectrum-Form-itemField")
|
cy.get(".spectrum-Form-itemField")
|
||||||
.eq(2)
|
.eq(3)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Picker-label").click({ force: true })
|
cy.get(".spectrum-Picker-label").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Menu").within(() => {
|
cy.get(".spectrum-Menu").within(() => {
|
||||||
cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true })
|
cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Form-itemField").eq(2).should("contain", role)
|
cy.get(".spectrum-Form-itemField").eq(3).should("contain", role)
|
||||||
})
|
})
|
||||||
|
|
||||||
// APPLICATIONS
|
// APPLICATIONS
|
||||||
|
@ -162,7 +164,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
||||||
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||||
cy.wait(1000)
|
cy.url({ timeout: 30000 }).should("include", "/apps")
|
||||||
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
|
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
|
||||||
|
|
||||||
// If apps already exist
|
// If apps already exist
|
||||||
|
@ -432,6 +434,7 @@ Cypress.Commands.add("createAppFromScratch", appName => {
|
||||||
|
|
||||||
// TABLES
|
// TABLES
|
||||||
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
|
// Creates an internal Budibase DB table
|
||||||
if (!initialTable) {
|
if (!initialTable) {
|
||||||
cy.navigateToDataSection()
|
cy.navigateToDataSection()
|
||||||
cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click()
|
cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click()
|
||||||
|
@ -445,6 +448,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
.contains("Continue")
|
.contains("Continue")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
})
|
})
|
||||||
|
cy.get(".spectrum-Modal").contains("Create Table", { timeout: 10000 })
|
||||||
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
|
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
|
||||||
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
|
@ -735,8 +739,15 @@ Cypress.Commands.add("deleteAllScreens", () => {
|
||||||
Cypress.Commands.add("navigateToFrontend", () => {
|
Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
// Clicks on Design tab and then the Home nav item
|
// Clicks on Design tab and then the Home nav item
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
|
cy.intercept("**/preview").as("preview")
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".spectrum-Search", { timeout: 2000 }).type("/")
|
cy.wait("@preview")
|
||||||
|
cy.get("@preview").then(res => {
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
cy.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Search", { timeout: 20000 }).type("/")
|
||||||
cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true })
|
cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.2.58",
|
"version": "1.2.59",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -13,11 +13,11 @@
|
||||||
"cy:setup:ci": "node ./cypress/setup.js",
|
"cy:setup:ci": "node ./cypress/setup.js",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js",
|
"cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js",
|
||||||
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
|
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
|
||||||
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
|
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
|
||||||
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
|
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
|
||||||
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report",
|
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record; npm run cy:ci:report",
|
||||||
"cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline",
|
"cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline",
|
||||||
"cy:ci:notify": "node scripts/cypressResultsWebhook",
|
"cy:ci:notify": "node scripts/cypressResultsWebhook",
|
||||||
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
|
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.2.58",
|
"@budibase/bbui": "^1.2.59",
|
||||||
"@budibase/client": "^1.2.58",
|
"@budibase/client": "^1.2.59",
|
||||||
"@budibase/frontend-core": "^1.2.58",
|
"@budibase/frontend-core": "^1.2.59",
|
||||||
"@budibase/string-templates": "^1.2.58",
|
"@budibase/string-templates": "^1.2.59",
|
||||||
"@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",
|
||||||
|
|
|
@ -5,7 +5,6 @@ const path = require("path")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
|
||||||
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL
|
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL
|
||||||
const OUTCOME = process.env.CYPRESS_OUTCOME
|
|
||||||
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL
|
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL
|
||||||
const GIT_SHA = process.env.GITHUB_SHA
|
const GIT_SHA = process.env.GITHUB_SHA
|
||||||
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
|
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
|
||||||
|
@ -35,6 +34,8 @@ async function discordCypressResultsNotification(report) {
|
||||||
skipped,
|
skipped,
|
||||||
} = report.stats
|
} = report.stats
|
||||||
|
|
||||||
|
const OUTCOME = failures > 0 ? "failure" : "success"
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -114,7 +115,7 @@ async function discordCypressResultsNotification(report) {
|
||||||
}
|
}
|
||||||
const response = await fetch(WEBHOOK_URL, options)
|
const response = await fetch(WEBHOOK_URL, options)
|
||||||
|
|
||||||
if (response.status >= 400) {
|
if (response.status >= 201) {
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
console.error(
|
console.error(
|
||||||
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`
|
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
makeComponentUnique,
|
makeComponentUnique,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants"
|
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
|
@ -40,6 +39,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
devicePreview: false,
|
devicePreview: false,
|
||||||
messagePassing: false,
|
messagePassing: false,
|
||||||
continueIfAction: false,
|
continueIfAction: false,
|
||||||
|
showNotificationAction: false,
|
||||||
},
|
},
|
||||||
errors: [],
|
errors: [],
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
|
@ -124,35 +124,6 @@ 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 => {
|
||||||
|
@ -534,7 +505,16 @@ export const getFrontendStore = () => {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let parentId
|
|
||||||
|
// Determine the next component to select after deletion
|
||||||
|
const state = get(store)
|
||||||
|
let nextSelectedComponentId
|
||||||
|
if (state.selectedComponentId === component._id) {
|
||||||
|
nextSelectedComponentId = store.actions.components.getNext()
|
||||||
|
if (!nextSelectedComponentId) {
|
||||||
|
nextSelectedComponentId = store.actions.components.getPrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Patch screen
|
// Patch screen
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
|
@ -549,17 +529,18 @@ export const getFrontendStore = () => {
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
parentId = parent._id
|
|
||||||
parent._children = parent._children.filter(
|
parent._children = parent._children.filter(
|
||||||
child => child._id !== component._id
|
child => child._id !== component._id
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select the deleted component's parent
|
// Update selected component if required
|
||||||
store.update(state => {
|
if (nextSelectedComponentId) {
|
||||||
state.selectedComponentId = parentId
|
store.update(state => {
|
||||||
return state
|
state.selectedComponentId = nextSelectedComponentId
|
||||||
})
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
copy: (component, cut = false, selectParent = true) => {
|
copy: (component, cut = false, selectParent = true) => {
|
||||||
// Update store with copied component
|
// Update store with copied component
|
||||||
|
@ -618,6 +599,16 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check inside is valid
|
||||||
|
if (mode === "inside") {
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
targetComponent._component
|
||||||
|
)
|
||||||
|
if (!definition.hasChildren) {
|
||||||
|
mode = "below"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Paste new component
|
// Paste new component
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
// Paste inside target component if chosen
|
// Paste inside target component if chosen
|
||||||
|
@ -654,46 +645,193 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getPrevious: () => {
|
||||||
|
const state = get(store)
|
||||||
|
const componentId = state.selectedComponentId
|
||||||
|
const screen = get(selectedScreen)
|
||||||
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
|
// Check we aren't right at the top of the tree
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
if (!parent || componentId === screen.props._id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have siblings above us, choose the sibling or a descendant
|
||||||
|
if (index > 0) {
|
||||||
|
// If sibling before us accepts children, select a descendant
|
||||||
|
const previousSibling = parent._children[index - 1]
|
||||||
|
if (previousSibling._children?.length) {
|
||||||
|
let target = previousSibling
|
||||||
|
while (target._children?.length) {
|
||||||
|
target = target._children[target._children.length - 1]
|
||||||
|
}
|
||||||
|
return target._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just select sibling
|
||||||
|
return previousSibling._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no siblings above us, select the parent
|
||||||
|
return parent._id
|
||||||
|
},
|
||||||
|
getNext: () => {
|
||||||
|
const component = get(selectedComponent)
|
||||||
|
const componentId = component?._id
|
||||||
|
const screen = get(selectedScreen)
|
||||||
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
|
||||||
|
// If we have children, select first child
|
||||||
|
if (component._children?.length) {
|
||||||
|
return component._children[0]._id
|
||||||
|
} else if (!parent) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise select the next sibling if we have one
|
||||||
|
if (index < parent._children.length - 1) {
|
||||||
|
const nextSibling = parent._children[index + 1]
|
||||||
|
return nextSibling._id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last child, select our parents next sibling
|
||||||
|
let target = parent
|
||||||
|
let targetParent = findComponentParent(screen.props, target._id)
|
||||||
|
let targetIndex = targetParent?._children.findIndex(
|
||||||
|
child => child._id === target._id
|
||||||
|
)
|
||||||
|
while (
|
||||||
|
targetParent != null &&
|
||||||
|
targetIndex === targetParent._children?.length - 1
|
||||||
|
) {
|
||||||
|
target = targetParent
|
||||||
|
targetParent = findComponentParent(screen.props, target._id)
|
||||||
|
targetIndex = targetParent?._children.findIndex(
|
||||||
|
child => child._id === target._id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (targetParent) {
|
||||||
|
return targetParent._children[targetIndex + 1]._id
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectPrevious: () => {
|
||||||
|
const previousId = store.actions.components.getPrevious()
|
||||||
|
if (previousId) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = previousId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectNext: () => {
|
||||||
|
const nextId = store.actions.components.getNext()
|
||||||
|
if (nextId) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedComponentId = nextId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
moveUp: async component => {
|
moveUp: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
if (!parent?._children?.length) {
|
|
||||||
return false
|
// Check we aren't right at the top of the tree
|
||||||
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
|
if (!parent || (index === 0 && parent._id === screen.props._id)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const currentIndex = parent._children.findIndex(
|
|
||||||
child => child._id === componentId
|
// Copy original component and remove it from the parent
|
||||||
)
|
const originalComponent = cloneDeep(parent._children[index])
|
||||||
if (currentIndex === 0) {
|
parent._children = parent._children.filter(
|
||||||
return false
|
|
||||||
}
|
|
||||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
|
||||||
const newChildren = parent._children.filter(
|
|
||||||
component => component._id !== componentId
|
component => component._id !== componentId
|
||||||
)
|
)
|
||||||
newChildren.splice(currentIndex - 1, 0, originalComponent)
|
|
||||||
parent._children = newChildren
|
// If we have siblings above us, move up
|
||||||
|
if (index > 0) {
|
||||||
|
// If sibling before us accepts children, move to last child of
|
||||||
|
// sibling
|
||||||
|
const previousSibling = parent._children[index - 1]
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
previousSibling._component
|
||||||
|
)
|
||||||
|
if (definition.hasChildren) {
|
||||||
|
previousSibling._children.push(originalComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just move component above sibling
|
||||||
|
else {
|
||||||
|
parent._children.splice(index - 1, 0, originalComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no siblings above us, go above the parent as long as it isn't
|
||||||
|
// the screen
|
||||||
|
else if (parent._id !== screen.props._id) {
|
||||||
|
const grandParent = findComponentParent(screen.props, parent._id)
|
||||||
|
const parentIndex = grandParent._children.findIndex(
|
||||||
|
child => child._id === parent._id
|
||||||
|
)
|
||||||
|
grandParent._children.splice(parentIndex, 0, originalComponent)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
moveDown: async component => {
|
moveDown: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
|
// Sanity check parent is found
|
||||||
if (!parent?._children?.length) {
|
if (!parent?._children?.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const currentIndex = parent._children.findIndex(
|
|
||||||
child => child._id === componentId
|
// Check we aren't right at the bottom of the tree
|
||||||
)
|
const index = parent._children.findIndex(x => x._id === componentId)
|
||||||
if (currentIndex === parent._children.length - 1) {
|
if (
|
||||||
return false
|
index === parent._children.length - 1 &&
|
||||||
|
parent._id === screen.props._id
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
|
||||||
const newChildren = parent._children.filter(
|
// Copy the original component and remove from parent
|
||||||
|
const originalComponent = cloneDeep(parent._children[index])
|
||||||
|
parent._children = parent._children.filter(
|
||||||
component => component._id !== componentId
|
component => component._id !== componentId
|
||||||
)
|
)
|
||||||
newChildren.splice(currentIndex + 1, 0, originalComponent)
|
|
||||||
parent._children = newChildren
|
// Move below the next sibling if we are not the last sibling
|
||||||
|
if (index < parent._children.length) {
|
||||||
|
// If the next sibling has children, become the first child
|
||||||
|
const nextSibling = parent._children[index]
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
nextSibling._component
|
||||||
|
)
|
||||||
|
if (definition.hasChildren) {
|
||||||
|
nextSibling._children.splice(0, 0, originalComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise move below next sibling
|
||||||
|
else {
|
||||||
|
parent._children.splice(index + 1, 0, originalComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last child, so move below our parent
|
||||||
|
else {
|
||||||
|
const grandParent = findComponentParent(screen.props, parent._id)
|
||||||
|
const parentIndex = grandParent._children.findIndex(
|
||||||
|
child => child._id === parent._id
|
||||||
|
)
|
||||||
|
grandParent._children.splice(parentIndex + 1, 0, originalComponent)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateStyle: async (name, value) => {
|
updateStyle: async (name, value) => {
|
||||||
|
|
|
@ -162,7 +162,7 @@
|
||||||
width="28px"
|
width="28px"
|
||||||
height="28px"
|
height="28px"
|
||||||
class="spectrum-Icon"
|
class="spectrum-Icon"
|
||||||
style="color:grey;"
|
style="color:var(--spectrum-global-color-gray-700);"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
width="28px"
|
width="28px"
|
||||||
height="28px"
|
height="28px"
|
||||||
class="spectrum-Icon"
|
class="spectrum-Icon"
|
||||||
style="color:grey;"
|
style="color:var(--spectrum-global-color-gray-700);"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="automations-list">
|
<div class="automations-list">
|
||||||
{#each $automationStore.automations as automation, idx}
|
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
|
||||||
<NavItem
|
<NavItem
|
||||||
border={idx > 0}
|
border={idx > 0}
|
||||||
icon="ShareAndroid"
|
icon="ShareAndroid"
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<HideAutocolumnButton bind:hideAutocolumns />
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
<ImportButton
|
<ImportButton
|
||||||
|
disabled={$tables.selected?._id === "ta_users"}
|
||||||
tableId={$tables.selected?._id}
|
tableId={$tables.selected?._id}
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
import ImportModal from "../modals/ImportModal.svelte"
|
import ImportModal from "../modals/ImportModal.svelte"
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
|
export let disabled
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show}>
|
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show} {disabled}>
|
||||||
Import
|
Import
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -467,6 +467,7 @@
|
||||||
options={relationshipOptions}
|
options={relationshipOptions}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
getOptionValue={option => option.value}
|
getOptionValue={option => option.value}
|
||||||
|
getOptionTitle={option => option.alt}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth, apps } from "stores/portal"
|
import { auth, apps } from "stores/portal"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
@ -72,62 +74,67 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={appLockModal}>
|
{#key app}
|
||||||
<ModalContent
|
<div>
|
||||||
title={lockedByHeading}
|
<Modal bind:this={appLockModal}>
|
||||||
dataCy={"app-lock-modal"}
|
<ModalContent
|
||||||
showConfirmButton={false}
|
title={lockedByHeading}
|
||||||
showCancelButton={false}
|
dataCy={"app-lock-modal"}
|
||||||
>
|
showConfirmButton={false}
|
||||||
<p>
|
showCancelButton={false}
|
||||||
Apps are locked to prevent work from being lost from overlapping changes
|
>
|
||||||
between your team.
|
<Layout noPadding>
|
||||||
</p>
|
<Body size="S">
|
||||||
|
Apps are locked to prevent work from being lost from overlapping
|
||||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
changes between your team.
|
||||||
<span class="lock-expiry-body">
|
</Body>
|
||||||
{processStringSync(
|
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
<span class="lock-expiry-body">
|
||||||
{
|
{processStringSync(
|
||||||
time: getExpiryDuration(app),
|
"This lock will expire in {{ duration time 'millisecond' }} from now. This lock will expire in This lock will expire in ",
|
||||||
}
|
{
|
||||||
)}
|
time: getExpiryDuration(app),
|
||||||
</span>
|
}
|
||||||
{/if}
|
)}
|
||||||
<div class="lock-modal-actions">
|
</span>
|
||||||
<ButtonGroup>
|
{/if}
|
||||||
<Button
|
<div class="lock-modal-actions">
|
||||||
secondary
|
<ButtonGroup>
|
||||||
quiet={lockedBy && lockedByYou}
|
<Button
|
||||||
disabled={processing}
|
secondary
|
||||||
on:click={() => {
|
quiet={lockedBy && lockedByYou}
|
||||||
appLockModal.hide()
|
disabled={processing}
|
||||||
}}
|
on:click={() => {
|
||||||
>
|
appLockModal.hide()
|
||||||
<span class="cancel"
|
}}
|
||||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
>
|
||||||
>
|
<span class="cancel"
|
||||||
</Button>
|
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||||
{#if lockedByYou}
|
>
|
||||||
<Button
|
</Button>
|
||||||
secondary
|
{#if lockedByYou}
|
||||||
disabled={processing}
|
<Button
|
||||||
on:click={() => {
|
secondary
|
||||||
releaseLock()
|
disabled={processing}
|
||||||
appLockModal.hide()
|
on:click={() => {
|
||||||
}}
|
releaseLock()
|
||||||
>
|
appLockModal.hide()
|
||||||
{#if processing}
|
}}
|
||||||
<ProgressCircle overBackground={true} size="S" />
|
>
|
||||||
{:else}
|
{#if processing}
|
||||||
<span class="unlock">Release Lock</span>
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
{/if}
|
{:else}
|
||||||
</Button>
|
<span class="unlock">Release Lock</span>
|
||||||
{/if}
|
{/if}
|
||||||
</ButtonGroup>
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
</ModalContent>
|
</ButtonGroup>
|
||||||
</Modal>
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.lock-modal-actions {
|
.lock-modal-actions {
|
||||||
|
|
|
@ -27,6 +27,14 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteAttachments(fileList) {
|
||||||
|
try {
|
||||||
|
return await API.deleteBuilderAttachments(fileList)
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
@ -34,5 +42,6 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
|
{deleteAttachments}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
export let scrollable = false
|
export let scrollable = false
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
|
export let id
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
|
{id}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
Tab,
|
Tab,
|
||||||
Body,
|
Body,
|
||||||
Layout,
|
Layout,
|
||||||
|
Button,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -15,10 +16,15 @@
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
import { addHBSBinding, addJSBinding } from "./utils"
|
import { addHBSBinding, addJSBinding } from "./utils"
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -62,15 +68,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a HBS helper to the expression
|
// Adds a JS/HBS helper to the expression
|
||||||
const addHelper = helper => {
|
const addHelper = (helper, js) => {
|
||||||
hbsValue = addHBSBinding(hbsValue, getCaretPosition(), helper.text)
|
let tempVal
|
||||||
updateValue(hbsValue)
|
const pos = getCaretPosition()
|
||||||
|
if (js) {
|
||||||
|
const decoded = decodeJSBinding(jsValue)
|
||||||
|
tempVal = jsValue = encodeJSBinding(
|
||||||
|
addJSBinding(decoded, pos, helper.text, { helper: true })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
||||||
|
}
|
||||||
|
updateValue(tempVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a data binding to the expression
|
// Adds a data binding to the expression
|
||||||
const addBinding = binding => {
|
const addBinding = (binding, { forceJS } = {}) => {
|
||||||
if (usingJS) {
|
if (usingJS || forceJS) {
|
||||||
let js = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||||
jsValue = encodeJSBinding(js)
|
jsValue = encodeJSBinding(js)
|
||||||
|
@ -100,6 +115,26 @@
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convert = () => {
|
||||||
|
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||||
|
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||||
|
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||||
|
hbsValue = null
|
||||||
|
mode = "JavaScript"
|
||||||
|
addBinding("", { forceJS: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHelperExample = (helper, js) => {
|
||||||
|
let example = helper.example || ""
|
||||||
|
if (js) {
|
||||||
|
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||||
|
if (example === "null;") {
|
||||||
|
example = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return example || ""
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, value))
|
valid = isValid(readableToRuntimeBinding(bindings, value))
|
||||||
})
|
})
|
||||||
|
@ -135,18 +170,21 @@
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if filteredHelpers?.length && !usingJS}
|
{#if filteredHelpers?.length}
|
||||||
<section>
|
<section>
|
||||||
<div class="heading">Helpers</div>
|
<div class="heading">Helpers</div>
|
||||||
<ul>
|
<ul>
|
||||||
{#each filteredHelpers as helper}
|
{#each filteredHelpers as helper}
|
||||||
<li on:click={() => addHelper(helper)}>
|
<li on:click={() => addHelper(helper, usingJS)}>
|
||||||
<div class="helper">
|
<div class="helper">
|
||||||
<div class="helper__name">{helper.displayText}</div>
|
<div class="helper__name">{helper.displayText}</div>
|
||||||
<div class="helper__description">
|
<div class="helper__description">
|
||||||
{@html helper.description}
|
{@html helper.description}
|
||||||
</div>
|
</div>
|
||||||
<pre class="helper__example">{helper.example || ""}</pre>
|
<pre class="helper__example">{getHelperExample(
|
||||||
|
helper,
|
||||||
|
usingJS
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -172,6 +210,11 @@
|
||||||
for more details.
|
for more details.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $admin.isDev}
|
||||||
|
<div class="convert">
|
||||||
|
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{#if allowJS}
|
{#if allowJS}
|
||||||
|
@ -306,4 +349,8 @@
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.convert {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,10 +18,14 @@ export function addHBSBinding(value, caretPos, binding) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addJSBinding(value, caretPos, binding) {
|
export function addJSBinding(value, caretPos, binding, { helper } = {}) {
|
||||||
binding = typeof binding === "string" ? binding : binding.path
|
binding = typeof binding === "string" ? binding : binding.path
|
||||||
value = value == null ? "" : value
|
value = value == null ? "" : value
|
||||||
binding = `$("${binding}")`
|
if (!helper) {
|
||||||
|
binding = `$("${binding}")`
|
||||||
|
} else {
|
||||||
|
binding = `helper.${binding}()`
|
||||||
|
}
|
||||||
if (caretPos.start) {
|
if (caretPos.start) {
|
||||||
value =
|
value =
|
||||||
value.substring(0, caretPos.start) +
|
value.substring(0, caretPos.start) +
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label, Checkbox } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
export let bindings = []
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
{
|
||||||
|
label: "Success",
|
||||||
|
value: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Warning",
|
||||||
|
value: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error",
|
||||||
|
value: "error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Info",
|
||||||
|
value: "info",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "success"
|
||||||
|
}
|
||||||
|
if (parameters.autoDismiss == null) {
|
||||||
|
parameters.autoDismiss = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label>Type</Label>
|
||||||
|
<Select bind:value={parameters.type} options={types} placeholder={null} />
|
||||||
|
<Label>Message</Label>
|
||||||
|
<DrawerBindableInput
|
||||||
|
{bindings}
|
||||||
|
value={parameters.message}
|
||||||
|
on:change={e => (parameters.message = e.detail)}
|
||||||
|
/>
|
||||||
|
<Label />
|
||||||
|
<Checkbox text="Auto dismiss" bind:value={parameters.autoDismiss} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,3 +15,4 @@ export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
export { default as ExportData } from "./ExportData.svelte"
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||||
|
export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||||
|
|
|
@ -110,6 +110,12 @@
|
||||||
"type": "logic",
|
"type": "logic",
|
||||||
"component": "ContinueIf",
|
"component": "ContinueIf",
|
||||||
"dependsOnFeature": "continueIfAction"
|
"dependsOnFeature": "continueIfAction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Show Notification",
|
||||||
|
"type": "application",
|
||||||
|
"component": "ShowNotification",
|
||||||
|
"dependsOnFeature": "showNotificationAction"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -3,29 +3,41 @@
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
|
Multiselect,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
Layout,
|
||||||
Select,
|
Select,
|
||||||
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getFields } from "helpers/searchFields"
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let allowBindings = true
|
export let allowBindings = true
|
||||||
|
export let allOr = false
|
||||||
|
|
||||||
|
$: dispatch("change", filters)
|
||||||
$: enrichedSchemaFields = getFields(schemaFields || [])
|
$: enrichedSchemaFields = getFields(schemaFields || [])
|
||||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
|
let behaviourValue
|
||||||
|
const behaviourOptions = [
|
||||||
|
{ value: "and", label: "Match all of the following filters" },
|
||||||
|
{ value: "or", label: "Match any of the following filters" },
|
||||||
|
]
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
filters = [
|
filters = [
|
||||||
...filters,
|
...filters,
|
||||||
|
@ -69,7 +81,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// if changed to an array, change default value to empty array
|
// if changed to an array, change default value to empty array
|
||||||
const idx = filters.findIndex(x => x.field === field)
|
const idx = filters.findIndex(x => x.id === expression.id)
|
||||||
if (expression.type === "array") {
|
if (expression.type === "array") {
|
||||||
filters[idx].value = []
|
filters[idx].value = []
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,12 +98,26 @@
|
||||||
if (expression.noValue) {
|
if (expression.noValue) {
|
||||||
expression.value = null
|
expression.value = null
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
operator === Constants.OperatorOptions.In.value &&
|
||||||
|
!Array.isArray(expression.value)
|
||||||
|
) {
|
||||||
|
if (expression.value) {
|
||||||
|
expression.value = [expression.value]
|
||||||
|
} else {
|
||||||
|
expression.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
const getFieldOptions = field => {
|
||||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
const schema = enrichedSchemaFields.find(x => x.name === field)
|
||||||
return schema?.constraints?.inclusion || []
|
return schema?.constraints?.inclusion || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
behaviourValue = allOr ? "or" : "and"
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
|
@ -107,79 +133,101 @@
|
||||||
</Body>
|
</Body>
|
||||||
{#if filters?.length}
|
{#if filters?.length}
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each filters as filter, idx}
|
<Select
|
||||||
<Select
|
label="Behaviour"
|
||||||
bind:value={filter.field}
|
value={behaviourValue}
|
||||||
options={fieldOptions}
|
options={behaviourOptions}
|
||||||
on:change={e => onFieldChange(filter, e.detail)}
|
getOptionLabel={opt => opt.label}
|
||||||
placeholder="Column"
|
getOptionValue={opt => opt.value}
|
||||||
/>
|
on:change={e => (allOr = e.detail === "or")}
|
||||||
<Select
|
placeholder={null}
|
||||||
disabled={!filter.field}
|
/>
|
||||||
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
</div>
|
||||||
bind:value={filter.operator}
|
<div>
|
||||||
on:change={e => onOperatorChange(filter, e.detail)}
|
<div class="filter-label">
|
||||||
placeholder={null}
|
<Label>Filters</Label>
|
||||||
/>
|
</div>
|
||||||
<Select
|
<div class="fields">
|
||||||
disabled={filter.noValue || !filter.field}
|
{#each filters as filter, idx}
|
||||||
options={valueTypeOptions}
|
<Select
|
||||||
bind:value={filter.valueType}
|
bind:value={filter.field}
|
||||||
placeholder={null}
|
options={fieldOptions}
|
||||||
/>
|
on:change={e => onFieldChange(filter, e.detail)}
|
||||||
{#if filter.valueType === "Binding"}
|
placeholder="Column"
|
||||||
<DrawerBindableInput
|
|
||||||
disabled={filter.noValue}
|
|
||||||
title={`Value for "${filter.field}"`}
|
|
||||||
value={filter.value}
|
|
||||||
placeholder="Value"
|
|
||||||
{panel}
|
|
||||||
{bindings}
|
|
||||||
on:change={event => (filter.value = event.detail)}
|
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
<Select
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
disabled={!filter.field}
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
||||||
<Combobox
|
bind:value={filter.operator}
|
||||||
disabled={filter.noValue}
|
on:change={e => onOperatorChange(filter, e.detail)}
|
||||||
options={getFieldOptions(filter.field)}
|
placeholder={null}
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "boolean"}
|
<Select
|
||||||
<Combobox
|
disabled={filter.noValue || !filter.field}
|
||||||
disabled={filter.noValue}
|
options={valueTypeOptions}
|
||||||
options={[
|
bind:value={filter.valueType}
|
||||||
{ label: "True", value: "true" },
|
placeholder={null}
|
||||||
{ label: "False", value: "false" },
|
|
||||||
]}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "datetime"}
|
{#if filter.valueType === "Binding"}
|
||||||
<DatePicker
|
<DrawerBindableInput
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
enableTime={!getSchema(filter).dateOnly}
|
title={`Value for "${filter.field}"`}
|
||||||
timeOnly={getSchema(filter).timeOnly}
|
value={filter.value}
|
||||||
bind:value={filter.value}
|
placeholder="Value"
|
||||||
|
{panel}
|
||||||
|
{bindings}
|
||||||
|
on:change={event => (filter.value = event.detail)}
|
||||||
|
/>
|
||||||
|
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||||
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
|
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
||||||
|
<Multiselect
|
||||||
|
disabled={filter.noValue}
|
||||||
|
options={getFieldOptions(filter.field)}
|
||||||
|
bind:value={filter.value}
|
||||||
|
/>
|
||||||
|
{:else if filter.type === "options"}
|
||||||
|
<Combobox
|
||||||
|
disabled={filter.noValue}
|
||||||
|
options={getFieldOptions(filter.field)}
|
||||||
|
bind:value={filter.value}
|
||||||
|
/>
|
||||||
|
{:else if filter.type === "boolean"}
|
||||||
|
<Combobox
|
||||||
|
disabled={filter.noValue}
|
||||||
|
options={[
|
||||||
|
{ label: "True", value: "true" },
|
||||||
|
{ label: "False", value: "false" },
|
||||||
|
]}
|
||||||
|
bind:value={filter.value}
|
||||||
|
/>
|
||||||
|
{:else if filter.type === "datetime"}
|
||||||
|
<DatePicker
|
||||||
|
disabled={filter.noValue}
|
||||||
|
enableTime={!getSchema(filter).dateOnly}
|
||||||
|
timeOnly={getSchema(filter).timeOnly}
|
||||||
|
bind:value={filter.value}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<DrawerBindableInput disabled />
|
||||||
|
{/if}
|
||||||
|
<Icon
|
||||||
|
name="Duplicate"
|
||||||
|
hoverable
|
||||||
|
size="S"
|
||||||
|
on:click={() => duplicateFilter(filter.id)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
<Icon
|
||||||
<DrawerBindableInput disabled />
|
name="Close"
|
||||||
{/if}
|
hoverable
|
||||||
<Icon
|
size="S"
|
||||||
name="Duplicate"
|
on:click={() => removeFilter(filter.id)}
|
||||||
hoverable
|
/>
|
||||||
size="S"
|
{/each}
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
</div>
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => removeFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div class="bottom">
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||||
Add filter
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -202,4 +250,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-template-columns: 1fr 120px 120px 1fr auto auto;
|
grid-template-columns: 1fr 120px 120px 1fr auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,21 +8,73 @@
|
||||||
import FilterDrawer from "./FilterDrawer.svelte"
|
import FilterDrawer from "./FilterDrawer.svelte"
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
|
|
||||||
|
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
|
||||||
let drawer
|
let drawer,
|
||||||
let tempValue = value || []
|
toSaveFilters = null,
|
||||||
|
allOr,
|
||||||
|
initialAllOr
|
||||||
|
|
||||||
|
$: initialFilters = correctFilters(value || [])
|
||||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
|
||||||
const saveFilter = async () => {
|
function addNumbering(filters) {
|
||||||
dispatch("change", tempValue)
|
let count = 1
|
||||||
|
for (let value of filters) {
|
||||||
|
if (value.field && value.field?.match(QUERY_START_REGEX) == null) {
|
||||||
|
value.field = `${count++}:${value.field}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
|
function correctFilters(filters) {
|
||||||
|
const corrected = []
|
||||||
|
for (let filter of filters) {
|
||||||
|
let field = filter.field
|
||||||
|
if (filter.operator === "allOr") {
|
||||||
|
initialAllOr = allOr = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof filter.field === "string" &&
|
||||||
|
filter.field.match(QUERY_START_REGEX) != null
|
||||||
|
) {
|
||||||
|
const parts = field.split(":")
|
||||||
|
const number = parts[0]
|
||||||
|
// it's the new format, remove number
|
||||||
|
if (!isNaN(parseInt(number))) {
|
||||||
|
parts.shift()
|
||||||
|
field = parts.join(":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
corrected.push({
|
||||||
|
...filter,
|
||||||
|
field,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return corrected
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFilter() {
|
||||||
|
if (!toSaveFilters && allOr !== initialAllOr) {
|
||||||
|
toSaveFilters = initialFilters
|
||||||
|
}
|
||||||
|
const filters = toSaveFilters?.filter(filter => filter.operator !== "allOr")
|
||||||
|
if (allOr && filters) {
|
||||||
|
filters.push({ operator: "allOr" })
|
||||||
|
}
|
||||||
|
// only save if anything was updated
|
||||||
|
if (filters) {
|
||||||
|
dispatch("change", addNumbering(filters))
|
||||||
|
}
|
||||||
notifications.success("Filters saved.")
|
notifications.success("Filters saved.")
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
@ -33,8 +85,12 @@
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<FilterDrawer
|
||||||
slot="body"
|
slot="body"
|
||||||
bind:filters={tempValue}
|
filters={initialFilters}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
bind:allOr
|
||||||
|
on:change={event => {
|
||||||
|
toSaveFilters = event.detail
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<span><AppLockModal {app} buttonSize="M" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
|
|
@ -55,13 +55,16 @@
|
||||||
let saveId, url
|
let saveId, url
|
||||||
let response, schema, enabledHeaders
|
let response, schema, enabledHeaders
|
||||||
let authConfigId
|
let authConfigId
|
||||||
let dynamicVariables, addVariableModal, varBinding
|
let dynamicVariables, addVariableModal, varBinding, globalDynamicBindings
|
||||||
let restBindings = getRestBindings()
|
let restBindings = getRestBindings()
|
||||||
|
|
||||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||||
|
|
||||||
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||||
$: dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
$: globalDynamicRequestBindings = toBindingsArray(
|
||||||
|
globalDynamicBindings,
|
||||||
|
"Dynamic"
|
||||||
|
)
|
||||||
$: dataSourceStaticBindings = toBindingsArray(
|
$: dataSourceStaticBindings = toBindingsArray(
|
||||||
staticVariables,
|
staticVariables,
|
||||||
"Datasource.Static"
|
"Datasource.Static"
|
||||||
|
@ -70,7 +73,7 @@
|
||||||
$: mergedBindings = [
|
$: mergedBindings = [
|
||||||
...restBindings,
|
...restBindings,
|
||||||
...customRequestBindings,
|
...customRequestBindings,
|
||||||
...dynamicRequestBindings,
|
...globalDynamicRequestBindings,
|
||||||
...dataSourceStaticBindings,
|
...dataSourceStaticBindings,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -231,11 +234,11 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
// convert dynamic variables list to simple key/val object
|
// convert dynamic variables list to simple key/val object
|
||||||
const getDynamicVariables = (datasource, queryId) => {
|
const getDynamicVariables = (datasource, queryId, matchFn) => {
|
||||||
const variablesList = datasource?.config?.dynamicVariables
|
const variablesList = datasource?.config?.dynamicVariables
|
||||||
if (variablesList && variablesList.length > 0) {
|
if (variablesList && variablesList.length > 0) {
|
||||||
const filtered = queryId
|
const filtered = queryId
|
||||||
? variablesList.filter(variable => variable.queryId === queryId)
|
? variablesList.filter(variable => matchFn(variable, queryId))
|
||||||
: variablesList
|
: variablesList
|
||||||
return filtered.reduce(
|
return filtered.reduce(
|
||||||
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
||||||
|
@ -367,12 +370,21 @@
|
||||||
if (query && !query.fields.pagination) {
|
if (query && !query.fields.pagination) {
|
||||||
query.fields.pagination = {}
|
query.fields.pagination = {}
|
||||||
}
|
}
|
||||||
dynamicVariables = getDynamicVariables(datasource, query._id)
|
dynamicVariables = getDynamicVariables(
|
||||||
|
datasource,
|
||||||
|
query._id,
|
||||||
|
(variable, queryId) => variable.queryId === queryId
|
||||||
|
)
|
||||||
|
globalDynamicBindings = getDynamicVariables(
|
||||||
|
datasource,
|
||||||
|
query._id,
|
||||||
|
(variable, queryId) => variable.queryId !== queryId
|
||||||
|
)
|
||||||
|
|
||||||
prettifyQueryRequestBody(
|
prettifyQueryRequestBody(
|
||||||
query,
|
query,
|
||||||
requestBindings,
|
requestBindings,
|
||||||
dynamicVariables,
|
globalDynamicBindings,
|
||||||
staticVariables,
|
staticVariables,
|
||||||
restBindings
|
restBindings
|
||||||
)
|
)
|
||||||
|
@ -437,7 +449,7 @@
|
||||||
valuePlaceholder="Default"
|
valuePlaceholder="Default"
|
||||||
bindings={[
|
bindings={[
|
||||||
...restBindings,
|
...restBindings,
|
||||||
...dynamicRequestBindings,
|
...globalDynamicRequestBindings,
|
||||||
...dataSourceStaticBindings,
|
...dataSourceStaticBindings,
|
||||||
]}
|
]}
|
||||||
bindingDrawerLeft="260px"
|
bindingDrawerLeft="260px"
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
selectedLayout,
|
selectedLayout,
|
||||||
currentAsset,
|
currentAsset,
|
||||||
} from "builderStore"
|
} from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import {
|
import {
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
@ -40,12 +39,6 @@
|
||||||
BUDIBASE: "type",
|
BUDIBASE: "type",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct iframe template
|
|
||||||
$: template = iframeTemplate.replace(
|
|
||||||
/\{\{ CLIENT_LIB_PATH }}/,
|
|
||||||
$store.clientLibPath
|
|
||||||
)
|
|
||||||
|
|
||||||
const placeholderScreen = new Screen()
|
const placeholderScreen = new Screen()
|
||||||
.name("Screen Placeholder")
|
.name("Screen Placeholder")
|
||||||
.route("/")
|
.route("/")
|
||||||
|
@ -151,7 +144,11 @@
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
// Legacy type, can be deleted in future
|
||||||
confirmDeleteComponent(data.id)
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "key-down") {
|
||||||
|
const { key, ctrlKey } = data
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
||||||
} else if (type === "duplicate-component" && data.id) {
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
const rootComponent = get(currentAsset).props
|
const rootComponent = get(currentAsset).props
|
||||||
const component = findComponent(rootComponent, data.id)
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
@ -293,7 +290,7 @@
|
||||||
<iframe
|
<iframe
|
||||||
title="componentPreview"
|
title="componentPreview"
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
srcdoc={template}
|
src="/preview"
|
||||||
class:hidden={loading || error}
|
class:hidden={loading || error}
|
||||||
class:tablet={$store.previewDevice === "tablet"}
|
class:tablet={$store.previewDevice === "tablet"}
|
||||||
class:mobile={$store.previewDevice === "mobile"}
|
class:mobile={$store.previewDevice === "mobile"}
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
export default `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src='{{ CLIENT_LIB_PATH }}'></script>
|
|
||||||
<script>
|
|
||||||
function receiveMessage(event) {
|
|
||||||
if (!event.data) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse received message
|
|
||||||
// If parsing fails, just ignore and wait for the next message
|
|
||||||
let parsed
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(event.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Client received invalid JSON")
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
if (!parsed || !parsed.isBudibaseEvent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract data from message
|
|
||||||
const {
|
|
||||||
selectedComponentId,
|
|
||||||
layout,
|
|
||||||
screen,
|
|
||||||
appId,
|
|
||||||
theme,
|
|
||||||
customTheme,
|
|
||||||
previewDevice,
|
|
||||||
navigation,
|
|
||||||
hiddenComponentIds
|
|
||||||
} = parsed
|
|
||||||
|
|
||||||
// Set some flags so the app knows we're in the builder
|
|
||||||
window["##BUDIBASE_IN_BUILDER##"] = true
|
|
||||||
window["##BUDIBASE_APP_ID##"] = appId
|
|
||||||
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
|
|
||||||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
|
||||||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
|
||||||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
|
||||||
window["##BUDIBASE_PREVIEW_THEME##"] = theme
|
|
||||||
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme
|
|
||||||
window["##BUDIBASE_PREVIEW_DEVICE##"] = previewDevice
|
|
||||||
window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation
|
|
||||||
window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds
|
|
||||||
|
|
||||||
// Initialise app
|
|
||||||
try {
|
|
||||||
if (window.loadBudibase) {
|
|
||||||
window.loadBudibase()
|
|
||||||
document.documentElement.classList.add("loaded")
|
|
||||||
} else {
|
|
||||||
throw "The client library couldn't be loaded"
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.parent.postMessage({ type: "error", error })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", receiveMessage)
|
|
||||||
window.parent.postMessage({ type: "ready" })
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body/>
|
|
||||||
</html>
|
|
||||||
`
|
|
|
@ -1,117 +1,79 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
$: definition = store.actions.components.getDefinition(component?._component)
|
|
||||||
$: noChildrenAllowed = !component || !definition?.hasChildren
|
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
// "editable" has been repurposed for inline text editing.
|
const keyboardEvent = (key, ctrlKey = false) => {
|
||||||
// It remains here for legacy compatibility.
|
document.dispatchEvent(
|
||||||
// Future components should define "static": true for indicate they should
|
new CustomEvent("component-menu", {
|
||||||
// not show a context menu.
|
detail: {
|
||||||
$: showMenu = definition?.editable !== false && definition?.static !== true
|
key,
|
||||||
|
ctrlKey,
|
||||||
const moveUpComponent = async () => {
|
id: component?._id,
|
||||||
try {
|
},
|
||||||
await store.actions.components.moveUp(component)
|
})
|
||||||
} catch (error) {
|
)
|
||||||
notifications.error("Error moving component up")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveDownComponent = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.components.moveDown(component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error moving component down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateComponent = () => {
|
|
||||||
storeComponentForCopy(false)
|
|
||||||
pasteComponent("below")
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteComponent = async () => {
|
|
||||||
try {
|
|
||||||
await store.actions.components.delete(component)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error deleting component")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeComponentForCopy = (cut = false) => {
|
|
||||||
store.actions.components.copy(component, cut)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pasteComponent = mode => {
|
|
||||||
try {
|
|
||||||
store.actions.components.paste(component, mode)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showMenu}
|
<ActionMenu>
|
||||||
<ActionMenu>
|
<div slot="control" class="icon">
|
||||||
<div slot="control" class="icon">
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
</div>
|
||||||
</div>
|
<MenuItem
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
icon="Delete"
|
||||||
Delete
|
keyBind="!BackAndroid"
|
||||||
</MenuItem>
|
on:click={() => keyboardEvent("Delete")}
|
||||||
<MenuItem noClose icon="ChevronUp" on:click={moveUpComponent}>
|
>
|
||||||
Move up
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem noClose icon="ChevronDown" on:click={moveDownComponent}>
|
<MenuItem
|
||||||
Move down
|
icon="ChevronUp"
|
||||||
</MenuItem>
|
keyBind="Ctrl+!ArrowUp"
|
||||||
<MenuItem noClose icon="Duplicate" on:click={duplicateComponent}>
|
on:click={() => keyboardEvent("ArrowUp", true)}
|
||||||
Duplicate
|
>
|
||||||
</MenuItem>
|
Move up
|
||||||
<MenuItem icon="Cut" on:click={() => storeComponentForCopy(true)}>
|
</MenuItem>
|
||||||
Cut
|
<MenuItem
|
||||||
</MenuItem>
|
icon="ChevronDown"
|
||||||
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
|
keyBind="Ctrl+!ArrowDown"
|
||||||
Copy
|
on:click={() => keyboardEvent("ArrowDown", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Move down
|
||||||
icon="LayersBringToFront"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("above")}
|
<MenuItem
|
||||||
disabled={noPaste}
|
icon="Duplicate"
|
||||||
>
|
keyBind="Ctrl+D"
|
||||||
Paste above
|
on:click={() => keyboardEvent("d", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Duplicate
|
||||||
icon="LayersSendToBack"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("below")}
|
<MenuItem
|
||||||
disabled={noPaste}
|
icon="Cut"
|
||||||
>
|
keyBind="Ctrl+X"
|
||||||
Paste below
|
on:click={() => keyboardEvent("x", true)}
|
||||||
</MenuItem>
|
>
|
||||||
<MenuItem
|
Cut
|
||||||
icon="ShowOneLayer"
|
</MenuItem>
|
||||||
on:click={() => pasteComponent("inside")}
|
<MenuItem
|
||||||
disabled={noPaste || noChildrenAllowed}
|
icon="Copy"
|
||||||
>
|
keyBind="Ctrl+C"
|
||||||
Paste inside
|
on:click={() => keyboardEvent("c", true)}
|
||||||
</MenuItem>
|
>
|
||||||
</ActionMenu>
|
Copy
|
||||||
<ConfirmDialog
|
</MenuItem>
|
||||||
bind:this={confirmDeleteDialog}
|
<MenuItem
|
||||||
title="Confirm Deletion"
|
icon="LayersSendToBack"
|
||||||
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
keyBind="Ctrl+V"
|
||||||
okText="Delete Component"
|
on:click={() => keyboardEvent("v", true)}
|
||||||
onOk={deleteComponent}
|
disabled={noPaste}
|
||||||
/>
|
>
|
||||||
{/if}
|
Paste
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { selectedComponent, selectedScreen, store } from "builderStore"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
let confirmDeleteDialog
|
||||||
|
let componentToDelete
|
||||||
|
|
||||||
|
const keyHandlers = {
|
||||||
|
["^ArrowUp"]: async component => {
|
||||||
|
await store.actions.components.moveUp(component)
|
||||||
|
},
|
||||||
|
["^ArrowDown"]: async component => {
|
||||||
|
await store.actions.components.moveDown(component)
|
||||||
|
},
|
||||||
|
["^c"]: component => {
|
||||||
|
store.actions.components.copy(component, false)
|
||||||
|
},
|
||||||
|
["^x"]: component => {
|
||||||
|
store.actions.components.copy(component, true)
|
||||||
|
},
|
||||||
|
["^v"]: async component => {
|
||||||
|
await store.actions.components.paste(component, "inside")
|
||||||
|
},
|
||||||
|
["^d"]: async component => {
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component, "below")
|
||||||
|
},
|
||||||
|
["^Enter"]: () => {
|
||||||
|
$goto("./new")
|
||||||
|
},
|
||||||
|
["Delete"]: component => {
|
||||||
|
// Don't show confirmation for the screen itself
|
||||||
|
if (component?._id === $selectedScreen.props._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
componentToDelete = component
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
},
|
||||||
|
["ArrowUp"]: () => {
|
||||||
|
store.actions.components.selectPrevious()
|
||||||
|
},
|
||||||
|
["ArrowDown"]: () => {
|
||||||
|
store.actions.components.selectNext()
|
||||||
|
},
|
||||||
|
["Escape"]: () => {
|
||||||
|
if (!$isActive("/new")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
$goto("./")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyAction = async (component, key, ctrlKey = false) => {
|
||||||
|
if (!component || !key) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Delete and backspace are the same
|
||||||
|
if (key === "Backspace") {
|
||||||
|
key = "Delete"
|
||||||
|
}
|
||||||
|
// Prefix key with a caret for ctrl modifier
|
||||||
|
if (ctrlKey) {
|
||||||
|
key = "^" + key
|
||||||
|
}
|
||||||
|
const handler = keyHandlers[key]
|
||||||
|
if (!handler) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return handler(component)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error handling key press")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = async e => {
|
||||||
|
// Ignore repeating events
|
||||||
|
if (e.repeat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore events when typing
|
||||||
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
|
if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Key events are always for the selected component
|
||||||
|
return handleKeyAction($selectedComponent, e.key, e.ctrlKey || e.metaKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComponentMenu = async e => {
|
||||||
|
// Menu events can be for any component
|
||||||
|
const { id, key, ctrlKey } = e.detail
|
||||||
|
const component = findComponent($selectedScreen.props, id)
|
||||||
|
return await handleKeyAction(component, key, ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("keydown", handleKeyPress)
|
||||||
|
document.addEventListener("component-menu", handleComponentMenu)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress)
|
||||||
|
document.removeEventListener("component-menu", handleComponentMenu)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
body={`Are you sure you want to delete "${componentToDelete?._instanceName}"?`}
|
||||||
|
okText="Delete Component"
|
||||||
|
onOk={() => store.actions.components.delete(componentToDelete)}
|
||||||
|
/>
|
|
@ -6,55 +6,11 @@
|
||||||
import { store, selectedScreen } from "builderStore"
|
import { store, selectedScreen } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
import { setContext } from "svelte"
|
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
import { DropPosition } from "./dndStore"
|
import { DropPosition } from "./dndStore"
|
||||||
import { notifications, Button } from "@budibase/bbui"
|
import { notifications, Button } from "@budibase/bbui"
|
||||||
|
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||||
let scrollRef
|
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||||
|
|
||||||
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 - 36
|
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -64,25 +20,15 @@
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set scroll context so components can invoke scrolling when selected
|
|
||||||
setContext("scroll", {
|
|
||||||
scrollTo,
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title="Components" showExpandIcon borderRight>
|
<Panel title="Components" showExpandIcon borderRight>
|
||||||
<div class="add-component">
|
<div class="add-component">
|
||||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-items-container" bind:this={scrollRef}>
|
<ComponentScrollWrapper>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li>
|
||||||
on:click={() => {
|
|
||||||
$store.selectedComponentId = $selectedScreen?.props._id
|
|
||||||
}}
|
|
||||||
id={`component-${$selectedScreen?.props._id}`}
|
|
||||||
>
|
|
||||||
<NavItem
|
<NavItem
|
||||||
text="Screen"
|
text="Screen"
|
||||||
indentLevel={0}
|
indentLevel={0}
|
||||||
|
@ -91,6 +37,10 @@
|
||||||
scrollable
|
scrollable
|
||||||
icon="WebPage"
|
icon="WebPage"
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
|
on:click={() => {
|
||||||
|
$store.selectedComponentId = $selectedScreen?.props._id
|
||||||
|
}}
|
||||||
|
id={`component-${$selectedScreen?.props._id}`}
|
||||||
>
|
>
|
||||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -114,8 +64,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</ComponentScrollWrapper>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<ComponentKeyHandler />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.add-component {
|
.add-component {
|
||||||
|
@ -125,12 +76,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.nav-items-container {
|
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import { dndStore } from "./dndStore"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
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 - 36
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set scroll context so components can invoke scrolling when selected
|
||||||
|
setContext("scroll", {
|
||||||
|
scrollTo,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDrop = async () => {
|
||||||
|
try {
|
||||||
|
await dndStore.actions.drop()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={scrollRef}
|
||||||
|
on:drop={onDrop}
|
||||||
|
ondragover="return false"
|
||||||
|
ondragenter="return false"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: var(--spacing-xl) 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -68,7 +68,8 @@
|
||||||
closedNodes = closedNodes
|
closedNodes = closedNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async e => {
|
||||||
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
await dndStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -31,15 +31,20 @@
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="Copy" on:click={() => storeComponentForCopy(false)}>
|
<MenuItem
|
||||||
|
icon="Copy"
|
||||||
|
keyBind="Ctrl+C"
|
||||||
|
on:click={() => storeComponentForCopy(false)}
|
||||||
|
>
|
||||||
Copy
|
Copy
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="ShowOneLayer"
|
icon="LayersSendToBack"
|
||||||
|
keyBind="Ctrl+V"
|
||||||
on:click={() => pasteComponent("inside")}
|
on:click={() => pasteComponent("inside")}
|
||||||
disabled={noPaste}
|
disabled={noPaste}
|
||||||
>
|
>
|
||||||
Paste inside
|
Paste
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -36,7 +36,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canRenderControl = setting => {
|
const canRenderControl = (setting, isScreen) => {
|
||||||
|
// Prevent rendering on click setting for screens
|
||||||
|
if (setting?.type === "event" && isScreen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const control = getComponentForSetting(setting)
|
const control = getComponentForSetting(setting)
|
||||||
if (!control) {
|
if (!control) {
|
||||||
return false
|
return false
|
||||||
|
@ -87,7 +92,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#each section.settings as setting (setting.key)}
|
{#each section.settings as setting (setting.key)}
|
||||||
{#if canRenderControl(setting)}
|
{#if canRenderControl(setting, isScreen)}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
type={setting.type}
|
type={setting.type}
|
||||||
control={getComponentForSetting(setting)}
|
control={getComponentForSetting(setting)}
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, Slider, Icon } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const ThemeOptions = [
|
const onChangeTheme = async theme => {
|
||||||
"spectrum--darkest",
|
|
||||||
"spectrum--dark",
|
|
||||||
"spectrum--light",
|
|
||||||
"spectrum--lightest",
|
|
||||||
]
|
|
||||||
|
|
||||||
$: themeIndex = ThemeOptions.indexOf($store.theme) ?? 2
|
|
||||||
|
|
||||||
const onChangeTheme = async e => {
|
|
||||||
try {
|
try {
|
||||||
const theme = ThemeOptions[e.detail] ?? ThemeOptions[2]
|
await store.actions.theme.save(`spectrum--${theme}`)
|
||||||
await store.actions.theme.save(theme)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating theme")
|
notifications.error("Error updating theme")
|
||||||
}
|
}
|
||||||
|
@ -22,26 +13,52 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Icon name="Moon" />
|
{#each Constants.Themes as theme}
|
||||||
<Slider
|
<div
|
||||||
min={0}
|
class="theme"
|
||||||
max={3}
|
class:selected={`spectrum--${theme.class}` === $store.theme}
|
||||||
step={1}
|
on:click={() => onChangeTheme(theme.class)}
|
||||||
value={themeIndex}
|
>
|
||||||
on:change={onChangeTheme}
|
<div
|
||||||
/>
|
style="background: {theme.preview}"
|
||||||
<Icon name="Light" />
|
class="color spectrum--{theme.class}"
|
||||||
|
/>
|
||||||
|
{theme.name}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.theme {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-xl);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
div :global(.spectrum-Form-item) {
|
.theme:hover {
|
||||||
flex: 1 1 auto;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.theme.selected,
|
||||||
|
.theme:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { Slider, Button } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let customTheme
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const options = ["0", "4px", "8px", "16px"]
|
||||||
|
|
||||||
|
$: index = options.indexOf(customTheme.buttonBorderRadius) ?? 2
|
||||||
|
|
||||||
|
const onChange = async e => {
|
||||||
|
dispatch("change", options[e.detail])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Slider min={0} max={3} step={1} value={index} on:change={onChange} />
|
||||||
|
<div class="button" style="--radius: {customTheme.buttonBorderRadius};">
|
||||||
|
<Button primary newStyles>Button</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.container :global(.spectrum-Form-item) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.button :global(.spectrum-Button) {
|
||||||
|
border-radius: var(--radius) !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,35 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import {
|
import { Layout, Label, ColorPicker, notifications } from "@budibase/bbui"
|
||||||
Layout,
|
|
||||||
Label,
|
|
||||||
ColorPicker,
|
|
||||||
Button,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { DefaultAppTheme } from "constants"
|
import { DefaultAppTheme } from "constants"
|
||||||
import AppThemeSelect from "./AppThemeSelect.svelte"
|
import AppThemeSelect from "./AppThemeSelect.svelte"
|
||||||
|
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
|
||||||
const ButtonBorderRadiusOptions = [
|
|
||||||
{
|
|
||||||
label: "Square",
|
|
||||||
value: "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Soft edge",
|
|
||||||
value: "4px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Curved",
|
|
||||||
value: "8px",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Round",
|
|
||||||
value: "16px",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
$: customTheme = $store.customTheme || {}
|
$: customTheme = $store.customTheme || {}
|
||||||
|
|
||||||
|
@ -52,22 +28,11 @@
|
||||||
<AppThemeSelect />
|
<AppThemeSelect />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Buttons</Label>
|
<Label>Button roundness</Label>
|
||||||
<div class="buttons">
|
<ButtonRoundnessSelect
|
||||||
{#each ButtonBorderRadiusOptions as option}
|
{customTheme}
|
||||||
<div
|
on:change={e => update("buttonBorderRadius", e.detail)}
|
||||||
class:active={customTheme.buttonBorderRadius === option.value}
|
/>
|
||||||
style={`--radius: ${option.value}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={() => update("buttonBorderRadius", option.value)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Accent color</Label>
|
<Label>Accent color</Label>
|
||||||
|
@ -88,29 +53,3 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<style>
|
|
||||||
.buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 100px 100px;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
.buttons > div {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
.buttons > div :global(.spectrum-Button) {
|
|
||||||
border-radius: var(--radius) !important;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: var(--spectrum-global-color-gray-400);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.buttons > div:hover :global(.spectrum-Button) {
|
|
||||||
background: var(--spectrum-global-color-gray-700);
|
|
||||||
border-color: var(--spectrum-global-color-gray-700);
|
|
||||||
}
|
|
||||||
.buttons > div.active :global(.spectrum-Button) {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
color: var(--spectrum-global-color-gray-800);
|
|
||||||
border-color: var(--spectrum-global-color-gray-600);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { users } from "stores/portal"
|
import { users, organisation } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
const inviteCode = $params["?code"]
|
const inviteCode = $params["?code"]
|
||||||
let password, error
|
let password, error
|
||||||
|
|
||||||
|
$: company = $organisation.company || "Budibase"
|
||||||
|
|
||||||
async function acceptInvite() {
|
async function acceptInvite() {
|
||||||
try {
|
try {
|
||||||
await users.acceptInvite(inviteCode, password)
|
await users.acceptInvite(inviteCode, password)
|
||||||
|
@ -17,16 +20,24 @@
|
||||||
notifications.error(error.message)
|
notifications.error(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await organisation.init()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting org config")
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Layout>
|
<Layout>
|
||||||
<img src={Logo} alt="logo" />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<Layout gap="XS" justifyItems="center" noPadding>
|
<Layout gap="XS" justifyItems="center" noPadding>
|
||||||
<Heading size="M">Accept Invitation</Heading>
|
<Heading size="M">Invitation to {company}</Heading>
|
||||||
<Body textAlign="center" size="M">
|
<Body textAlign="center" size="M">
|
||||||
Please enter a password to set up your user.
|
Please enter a password to get started.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<PasswordRepeatInput bind:error bind:password />
|
<PasswordRepeatInput bind:error bind:password />
|
||||||
|
@ -46,7 +57,7 @@
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 260px;
|
width: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
Body,
|
Body,
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
Tag,
|
||||||
|
Tags,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -29,6 +31,8 @@
|
||||||
OIDC: "oidc",
|
OIDC: "oidc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HasSpacesRegex = /[\\"\s]/
|
||||||
|
|
||||||
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
||||||
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
||||||
$: googleCallbackUrl = undefined
|
$: googleCallbackUrl = undefined
|
||||||
|
@ -145,7 +149,6 @@
|
||||||
|
|
||||||
async function save(docs) {
|
async function save(docs) {
|
||||||
let calls = []
|
let calls = []
|
||||||
|
|
||||||
// Only if the user has provided an image, upload it
|
// Only if the user has provided an image, upload it
|
||||||
if (image) {
|
if (image) {
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
|
@ -157,7 +160,6 @@
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
docs.forEach(element => {
|
docs.forEach(element => {
|
||||||
// Delete unsupported fields
|
// Delete unsupported fields
|
||||||
delete element.createdAt
|
delete element.createdAt
|
||||||
|
@ -199,7 +201,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (calls.length) {
|
if (calls.length) {
|
||||||
Promise.all(calls)
|
Promise.all(calls)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
@ -215,6 +216,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaultScopes = ["profile", "email", "offline_access"]
|
||||||
|
|
||||||
|
const refreshScopes = idx => {
|
||||||
|
providers.oidc.config.configs[idx]["scopes"] =
|
||||||
|
providers.oidc.config.configs[idx]["scopes"]
|
||||||
|
}
|
||||||
|
|
||||||
|
let scopesFields = [
|
||||||
|
{
|
||||||
|
editing: true,
|
||||||
|
inputText: null,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
@ -276,7 +292,7 @@
|
||||||
if (!oidcDoc?._id) {
|
if (!oidcDoc?._id) {
|
||||||
providers.oidc = {
|
providers.oidc = {
|
||||||
type: ConfigTypes.OIDC,
|
type: ConfigTypes.OIDC,
|
||||||
config: { configs: [{ activated: true }] },
|
config: { configs: [{ activated: true, scopes: defaultScopes }] },
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
originalOidcDoc = cloneDeep(oidcDoc)
|
originalOidcDoc = cloneDeep(oidcDoc)
|
||||||
|
@ -345,6 +361,7 @@
|
||||||
size="s"
|
size="s"
|
||||||
cta
|
cta
|
||||||
on:click={() => save([providers.oidc])}
|
on:click={() => save([providers.oidc])}
|
||||||
|
dataCy={"oidc-save"}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -362,6 +379,7 @@
|
||||||
bind:value={providers.oidc.config.configs[0][field.name]}
|
bind:value={providers.oidc.config.configs[0][field.name]}
|
||||||
readonly={field.readonly}
|
readonly={field.readonly}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
|
dataCy={field.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -392,15 +410,132 @@
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="L">Activated</Label>
|
<Label size="L">Activated</Label>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
dataCy={"oidc-active"}
|
||||||
text=""
|
text=""
|
||||||
bind:value={providers.oidc.config.configs[0].activated}
|
bind:value={providers.oidc.config.configs[0].activated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<span class="advanced-config">
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="XS">
|
||||||
|
<div class="auth-scopes">
|
||||||
|
<div>Advanced</div>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
newStyles
|
||||||
|
size="S"
|
||||||
|
on:click={() => {
|
||||||
|
providers.oidc.config.configs[0]["scopes"] = [...defaultScopes]
|
||||||
|
}}
|
||||||
|
dataCy={"restore-oidc-default-scopes"}
|
||||||
|
>
|
||||||
|
Restore Defaults
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Changes to your authentication scopes will only take effect when you
|
||||||
|
next log in. Please refer to your vendor documentation before
|
||||||
|
modification.
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<div class="auth-form">
|
||||||
|
<span class="add-new">
|
||||||
|
<Label size="L">{"Auth Scopes"}</Label>
|
||||||
|
<Input
|
||||||
|
dataCy={"new-scope-input"}
|
||||||
|
error={scopesFields[0].error}
|
||||||
|
placeholder={"New Scope"}
|
||||||
|
bind:value={scopesFields[0].inputText}
|
||||||
|
on:keyup={e => {
|
||||||
|
if (!scopesFields[0].inputText) {
|
||||||
|
scopesFields[0].error = null
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
e.key === "Enter" ||
|
||||||
|
e.keyCode === 13 ||
|
||||||
|
e.code == "Space" ||
|
||||||
|
e.keyCode == 32
|
||||||
|
) {
|
||||||
|
let scopes = providers.oidc.config.configs[0]["scopes"]
|
||||||
|
? providers.oidc.config.configs[0]["scopes"]
|
||||||
|
: [...defaultScopes]
|
||||||
|
|
||||||
|
let update = scopesFields[0].inputText.trim()
|
||||||
|
|
||||||
|
if (HasSpacesRegex.test(update)) {
|
||||||
|
scopesFields[0].error =
|
||||||
|
"Auth scopes cannot contain spaces, double quotes or backslashes"
|
||||||
|
return
|
||||||
|
} else if (scopes.indexOf(update) > -1) {
|
||||||
|
scopesFields[0].error = "Auth scope already exists"
|
||||||
|
return
|
||||||
|
} else if (!update.length) {
|
||||||
|
scopesFields[0].inputText = null
|
||||||
|
scopesFields[0].error = null
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
scopesFields[0].error = null
|
||||||
|
scopes.push(update)
|
||||||
|
providers.oidc.config.configs[0]["scopes"] = scopes
|
||||||
|
scopesFields[0].inputText = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div class="tag-wrap">
|
||||||
|
<span />
|
||||||
|
<Tags>
|
||||||
|
<Tag closable={false}>openid</Tag>
|
||||||
|
{#each providers.oidc.config.configs[0]["scopes"] || [...defaultScopes] as tag, idx}
|
||||||
|
<Tag
|
||||||
|
closable={scopesFields[0].editing}
|
||||||
|
on:click={() => {
|
||||||
|
let idxScopes = providers.oidc.config.configs[0]["scopes"]
|
||||||
|
if (idxScopes.length == 1) {
|
||||||
|
idxScopes.pop()
|
||||||
|
} else {
|
||||||
|
idxScopes.splice(idx, 1)
|
||||||
|
refreshScopes(0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
{/each}
|
||||||
|
</Tags>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.auth-scopes {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-config :global(.spectrum-Tags-item) {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form > * {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-config .auth-form .tag-wrap {
|
||||||
|
padding: 0px 5px 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 1fr;
|
grid-template-columns: 100px 1fr;
|
||||||
|
|
|
@ -44,7 +44,11 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateInput(email, index) {
|
function validateInput(input, index) {
|
||||||
|
if (input.email) {
|
||||||
|
input.email = input.email.trim()
|
||||||
|
}
|
||||||
|
const email = input.email
|
||||||
if (email) {
|
if (email) {
|
||||||
const res = emailValidator(email)
|
const res = emailValidator(email)
|
||||||
if (res === true) {
|
if (res === true) {
|
||||||
|
@ -61,7 +65,7 @@
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
let valid = true
|
let valid = true
|
||||||
userData.forEach((input, index) => {
|
userData.forEach((input, index) => {
|
||||||
valid = validateInput(input.email, index) && valid
|
valid = validateInput(input, index) && valid
|
||||||
})
|
})
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return false
|
return false
|
||||||
|
@ -95,7 +99,7 @@
|
||||||
bind:dropdownValue={input.role}
|
bind:dropdownValue={input.role}
|
||||||
options={Constants.BudibaseRoleOptions}
|
options={Constants.BudibaseRoleOptions}
|
||||||
error={input.error}
|
error={input.error}
|
||||||
on:blur={() => validateInput(input.email, index)}
|
on:blur={() => validateInput(input, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import { Body, ModalContent, Table } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let userData
|
||||||
|
export let deleteUsersResponse
|
||||||
|
|
||||||
|
let successCount
|
||||||
|
let failureCount
|
||||||
|
let title
|
||||||
|
let unsuccessfulUsers
|
||||||
|
let message
|
||||||
|
|
||||||
|
const setTitle = () => {
|
||||||
|
if (successCount) {
|
||||||
|
title = `${successCount} users deleted`
|
||||||
|
} else {
|
||||||
|
title = "Oops!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setMessage = () => {
|
||||||
|
if (successCount) {
|
||||||
|
message = "However there was a problem deleting some users."
|
||||||
|
} else {
|
||||||
|
message = "There was a problem deleting some users."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUsers = () => {
|
||||||
|
unsuccessfulUsers = deleteUsersResponse.unsuccessful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
reason: user.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
successCount = deleteUsersResponse.successful.length
|
||||||
|
failureCount = deleteUsersResponse.unsuccessful.length
|
||||||
|
setTitle()
|
||||||
|
setMessage()
|
||||||
|
setUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
email: {},
|
||||||
|
reason: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
size="M"
|
||||||
|
{title}
|
||||||
|
confirmText="Close"
|
||||||
|
showCloseIcon={false}
|
||||||
|
showCancelButton={false}
|
||||||
|
>
|
||||||
|
<Body size="XS">
|
||||||
|
{message}
|
||||||
|
</Body>
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
data={unsuccessfulUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -62,7 +62,7 @@
|
||||||
csvString = e.target.result
|
csvString = e.target.result
|
||||||
files = fileArray
|
files = fileArray
|
||||||
|
|
||||||
userEmails = csvString.split("\n")
|
userEmails = csvString.split(/\r?\n/)
|
||||||
})
|
})
|
||||||
reader.readAsText(fileArray[0])
|
reader.readAsText(fileArray[0])
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import { Body, ModalContent, Table } from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let inviteUsersResponse
|
||||||
|
|
||||||
|
let hasSuccess
|
||||||
|
let hasFailure
|
||||||
|
let title
|
||||||
|
let failureMessage
|
||||||
|
|
||||||
|
let unsuccessfulUsers
|
||||||
|
|
||||||
|
const setTitle = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
title = "Users invited!"
|
||||||
|
} else if (hasFailure) {
|
||||||
|
title = "Oops!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFailureMessage = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
failureMessage = "However there was a problem inviting some users."
|
||||||
|
} else {
|
||||||
|
failureMessage = "There was a problem inviting users."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUsers = () => {
|
||||||
|
unsuccessfulUsers = inviteUsersResponse.unsuccessful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
reason: user.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
hasSuccess = inviteUsersResponse.successful.length
|
||||||
|
hasFailure = inviteUsersResponse.unsuccessful.length
|
||||||
|
setTitle()
|
||||||
|
setFailureMessage()
|
||||||
|
setUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const failedSchema = {
|
||||||
|
email: {},
|
||||||
|
reason: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent showCancelButton={false} {title} confirmText="Done">
|
||||||
|
{#if hasSuccess}
|
||||||
|
<Body size="XS">
|
||||||
|
Your users should now receive an email invite to get access to their
|
||||||
|
Budibase account
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
{#if hasFailure}
|
||||||
|
<Body size="XS">
|
||||||
|
{failureMessage}
|
||||||
|
</Body>
|
||||||
|
<Table
|
||||||
|
schema={failedSchema}
|
||||||
|
data={unsuccessfulUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -2,24 +2,78 @@
|
||||||
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
|
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
|
||||||
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
|
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
|
||||||
import { parseToCsv } from "helpers/data/utils"
|
import { parseToCsv } from "helpers/data/utils"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let userData
|
export let userData
|
||||||
|
export let createUsersResponse
|
||||||
|
|
||||||
$: mappedData = userData.map(user => {
|
let hasSuccess
|
||||||
return {
|
let hasFailure
|
||||||
email: user.email,
|
let title
|
||||||
password: user.password,
|
let failureMessage
|
||||||
|
|
||||||
|
let userDataIndex
|
||||||
|
let successfulUsers
|
||||||
|
let unsuccessfulUsers
|
||||||
|
|
||||||
|
const setTitle = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
title = "Users created!"
|
||||||
|
} else if (hasFailure) {
|
||||||
|
title = "Oops!"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFailureMessage = () => {
|
||||||
|
if (hasSuccess) {
|
||||||
|
failureMessage = "However there was a problem creating some users."
|
||||||
|
} else {
|
||||||
|
failureMessage = "There was a problem creating some users."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUsers = () => {
|
||||||
|
userDataIndex = userData.reduce((prev, current) => {
|
||||||
|
prev[current.email] = current
|
||||||
|
return prev
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
successfulUsers = createUsersResponse.successful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
password: userDataIndex[user.email].password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
unsuccessfulUsers = createUsersResponse.unsuccessful.map(user => {
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
reason: user.reason,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
hasSuccess = createUsersResponse.successful.length
|
||||||
|
hasFailure = createUsersResponse.unsuccessful.length
|
||||||
|
setTitle()
|
||||||
|
setFailureMessage()
|
||||||
|
setUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
const schema = {
|
const successSchema = {
|
||||||
email: {},
|
email: {},
|
||||||
password: {},
|
password: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const failedSchema = {
|
||||||
|
email: {},
|
||||||
|
reason: {},
|
||||||
|
}
|
||||||
|
|
||||||
const downloadCsvFile = () => {
|
const downloadCsvFile = () => {
|
||||||
const fileName = "passwords.csv"
|
const fileName = "passwords.csv"
|
||||||
const content = parseToCsv(["email", "password"], mappedData)
|
const content = parseToCsv(["email", "password"], successfulUsers)
|
||||||
|
|
||||||
download(fileName, content)
|
download(fileName, content)
|
||||||
}
|
}
|
||||||
|
@ -43,35 +97,51 @@
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="M"
|
||||||
title="Accounts created!"
|
{title}
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body size="XS">
|
{#if hasFailure}
|
||||||
All your new users can be accessed through the autogenerated passwords. Take
|
<Body size="XS">
|
||||||
note of these passwords or download the CSV file.
|
{failureMessage}
|
||||||
</Body>
|
</Body>
|
||||||
|
<Table
|
||||||
|
schema={failedSchema}
|
||||||
|
data={unsuccessfulUsers}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if hasSuccess}
|
||||||
|
<Body size="XS">
|
||||||
|
All your new users can be accessed through the autogenerated passwords.
|
||||||
|
Take note of these passwords or download the CSV file.
|
||||||
|
</Body>
|
||||||
|
|
||||||
<div class="container" on:click={downloadCsvFile}>
|
<div class="container" on:click={downloadCsvFile}>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<Icon name="Download" />
|
<Icon name="Download" />
|
||||||
|
|
||||||
<div style="margin-left: var(--spacing-m)">
|
<div style="margin-left: var(--spacing-m)">
|
||||||
<Body size="XS">Passwords CSV</Body>
|
<Body size="XS">Passwords CSV</Body>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
{schema}
|
schema={successSchema}
|
||||||
data={mappedData}
|
data={successfulUsers}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]}
|
customRenderers={[
|
||||||
/>
|
{ column: "password", component: PasswordCopyRenderer },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
Table,
|
Table,
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
|
||||||
Search,
|
Search,
|
||||||
notifications,
|
notifications,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
@ -23,6 +22,8 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||||
|
import InvitedModal from "./_components/InvitedModal.svelte"
|
||||||
|
import DeletionFailureModal from "./_components/DeletionFailureModal.svelte"
|
||||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
@ -33,7 +34,8 @@
|
||||||
inviteConfirmationModal,
|
inviteConfirmationModal,
|
||||||
onboardingTypeModal,
|
onboardingTypeModal,
|
||||||
passwordModal,
|
passwordModal,
|
||||||
importUsersModal
|
importUsersModal,
|
||||||
|
deletionFailureModal
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let prevEmail = undefined,
|
let prevEmail = undefined,
|
||||||
searchEmail = undefined
|
searchEmail = undefined
|
||||||
|
@ -55,6 +57,9 @@
|
||||||
apps: {},
|
apps: {},
|
||||||
}
|
}
|
||||||
$: userData = []
|
$: userData = []
|
||||||
|
$: createUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
|
$: deleteUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
|
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, searchEmail)
|
$: fetchUsers(page, searchEmail)
|
||||||
$: {
|
$: {
|
||||||
|
@ -92,8 +97,7 @@
|
||||||
admin: user.role === Constants.BudibaseRoles.Admin,
|
admin: user.role === Constants.BudibaseRoles.Admin,
|
||||||
}))
|
}))
|
||||||
try {
|
try {
|
||||||
const res = await users.invite(payload)
|
inviteUsersResponse = await users.invite(payload)
|
||||||
notifications.success(res.message)
|
|
||||||
inviteConfirmationModal.show()
|
inviteConfirmationModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error inviting user")
|
notifications.error("Error inviting user")
|
||||||
|
@ -116,8 +120,9 @@
|
||||||
newUsers.push(user)
|
newUsers.push(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUsers.length)
|
if (!newUsers.length) {
|
||||||
notifications.info("Duplicated! There is no new users to add.")
|
notifications.info("Duplicated! There is no new users to add.")
|
||||||
|
}
|
||||||
return { ...userData, users: newUsers }
|
return { ...userData, users: newUsers }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,12 +144,14 @@
|
||||||
userData = await removingDuplicities({ groups, users })
|
userData = await removingDuplicities({ groups, users })
|
||||||
if (!userData.users.length) return
|
if (!userData.users.length) return
|
||||||
|
|
||||||
return createUser()
|
return createUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUser() {
|
async function createUsers() {
|
||||||
try {
|
try {
|
||||||
await users.create(await removingDuplicities(userData))
|
createUsersResponse = await users.create(
|
||||||
|
await removingDuplicities(userData)
|
||||||
|
)
|
||||||
notifications.success("Successfully created user")
|
notifications.success("Successfully created user")
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
passwordModal.show()
|
passwordModal.show()
|
||||||
|
@ -157,7 +164,7 @@
|
||||||
if (onboardingType === "emailOnboarding") {
|
if (onboardingType === "emailOnboarding") {
|
||||||
createUserFlow()
|
createUserFlow()
|
||||||
} else {
|
} else {
|
||||||
await createUser()
|
await createUsers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,8 +183,15 @@
|
||||||
notifications.error("You cannot delete yourself")
|
notifications.error("You cannot delete yourself")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await users.bulkDelete(ids)
|
deleteUsersResponse = await users.bulkDelete(ids)
|
||||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
if (deleteUsersResponse.unsuccessful?.length) {
|
||||||
|
deletionFailureModal.show()
|
||||||
|
} else {
|
||||||
|
notifications.success(
|
||||||
|
`Successfully deleted ${selectedRows.length} users`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
selectedRows = []
|
selectedRows = []
|
||||||
await fetchUsers(page, searchEmail)
|
await fetchUsers(page, searchEmail)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -267,16 +281,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={inviteConfirmationModal}>
|
<Modal bind:this={inviteConfirmationModal}>
|
||||||
<ModalContent
|
<InvitedModal {inviteUsersResponse} />
|
||||||
showCancelButton={false}
|
|
||||||
title="Invites sent!"
|
|
||||||
confirmText="Done"
|
|
||||||
>
|
|
||||||
<Body size="S"
|
|
||||||
>Your users should now recieve an email invite to get access to their
|
|
||||||
Budibase account</Body
|
|
||||||
></ModalContent
|
|
||||||
>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={onboardingTypeModal}>
|
<Modal bind:this={onboardingTypeModal}>
|
||||||
|
@ -284,7 +289,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={passwordModal}>
|
<Modal bind:this={passwordModal}>
|
||||||
<PasswordModal userData={userData.users} />
|
<PasswordModal {createUsersResponse} userData={userData.users} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={deletionFailureModal}>
|
||||||
|
<DeletionFailureModal {deleteUsersResponse} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={importUsersModal}>
|
<Modal bind:this={importUsersModal}>
|
||||||
|
|
|
@ -63,10 +63,14 @@ export function createUsersStore() {
|
||||||
|
|
||||||
return body
|
return body
|
||||||
})
|
})
|
||||||
await API.createUsers({ users: mappedUsers, groups: data.groups })
|
const response = await API.createUsers({
|
||||||
|
users: mappedUsers,
|
||||||
|
groups: data.groups,
|
||||||
|
})
|
||||||
|
|
||||||
// re-search from first page
|
// re-search from first page
|
||||||
await search()
|
await search()
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(id) {
|
async function del(id) {
|
||||||
|
@ -79,7 +83,7 @@ export function createUsersStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDelete(userIds) {
|
async function bulkDelete(userIds) {
|
||||||
await API.deleteUsers(userIds)
|
return API.deleteUsers(userIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(user) {
|
async function save(user) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.2.58",
|
"version": "1.2.59",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "1.1.32-alpha.6",
|
"@budibase/backend-core": "1.2.58-alpha.7",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue