1
0
Fork 0
mirror of synced 2024-06-22 16:10:40 +12:00

Merge branch 'develop' into user-fixes

This commit is contained in:
Rory Powell 2022-08-31 11:39:41 +01:00
commit 6077814823
151 changed files with 4467 additions and 1968 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -134,6 +134,18 @@ spec:
- name: NODE_DEBUG - name: NODE_DEBUG
value: {{ .Values.services.apps.nodeDebug | quote }} value: {{ .Values.services.apps.nodeDebug | quote }}
{{ end }} {{ end }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
image: budibase/apps:{{ .Values.globals.appVersion }} image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always

View file

@ -27,6 +27,8 @@ spec:
spec: spec:
containers: containers:
- env: - env:
- name: BUDIBASE_ENVIRONMENT
value: {{ .Values.globals.budibaseEnv }}
- name: DEPLOYMENT_ENVIRONMENT - name: DEPLOYMENT_ENVIRONMENT
value: "kubernetes" value: "kubernetes"
- name: CLUSTER_PORT - name: CLUSTER_PORT
@ -125,6 +127,19 @@ spec:
value: {{ .Values.globals.google.secret | quote }} value: {{ .Values.globals.google.secret | quote }}
- name: TENANT_FEATURE_FLAGS - name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }} value: {{ .Values.globals.tenantFeatureFlags | quote }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
image: budibase/worker:{{ .Values.globals.appVersion }} image: budibase/worker:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always
livenessProbe: livenessProbe:

View file

@ -114,6 +114,10 @@ globals:
smtp: smtp:
enabled: false enabled: false
# elasticApmEnabled:
# elasticApmSecretToken:
# elasticApmServerUrl:
services: services:
budibaseVersion: latest budibaseVersion: latest
dns: cluster.local dns: cluster.local

View file

@ -15,7 +15,10 @@ http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for" '
'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr';
access_log /var/log/nginx/access.log main;
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default "upgrade"; default "upgrade";
@ -62,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;

View file

@ -33,7 +33,10 @@ http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for" '
'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr';
access_log /var/log/nginx/access.log main;
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default "upgrade"; default "upgrade";
@ -85,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;
} }
@ -94,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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
{ {
"version": "1.2.47", "version": "1.2.57",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.2.47", "version": "1.2.57",
"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.47", "@budibase/types": "^1.2.57",
"@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",

View file

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

View file

@ -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,12 @@ function validate(schema, property) {
} else if (ctx.request[property] != null) { } else if (ctx.request[property] != null) {
params = ctx.request[property] params = ctx.request[property]
} }
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}`)

View file

@ -66,15 +66,13 @@ const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL]
* @constructor * @constructor
*/ */
export const ObjectStore = (bucket: any) => { export const ObjectStore = (bucket: any) => {
AWS.config.update({
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
region: env.AWS_REGION,
})
const config: any = { const config: any = {
s3ForcePathStyle: true, s3ForcePathStyle: true,
signatureVersion: "v4", signatureVersion: "v4",
apiVersion: "2006-03-01", apiVersion: "2006-03-01",
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
region: env.AWS_REGION,
} }
if (bucket) { if (bucket) {
config.params = { config.params = {

View file

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

View file

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.2.47", "version": "1.2.57",
"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.47", "@budibase/string-templates": "^1.2.57",
"@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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.2.47", "version": "1.2.57",
"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.47", "@budibase/bbui": "^1.2.57",
"@budibase/client": "^1.2.47", "@budibase/client": "^1.2.57",
"@budibase/frontend-core": "^1.2.47", "@budibase/frontend-core": "^1.2.57",
"@budibase/string-templates": "^1.2.47", "@budibase/string-templates": "^1.2.57",
"@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",

View file

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

View file

@ -40,6 +40,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,
@ -534,7 +535,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 +559,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 +629,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 +675,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) => {

View file

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

View file

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

View file

@ -14,7 +14,7 @@
$: { $: {
let fields = {} let fields = {}
for (const [key, type] of Object.entries(block?.inputs?.fields)) { for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
fields = { fields = {
...fields, ...fields,
[key]: { [key]: {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,117 +1,78 @@
<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. // Ensure this component is selected first
// Future components should define "static": true for indicate they should if (component._id !== $store.selectedComponentId) {
// not show a context menu. store.update(state => {
$: showMenu = definition?.editable !== false && definition?.static !== true state.selectedComponentId = component._id
return state
const moveUpComponent = async () => { })
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")
} }
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
} }
</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 {

View file

@ -2,16 +2,19 @@
import Panel from "components/design/Panel.svelte" import Panel from "components/design/Panel.svelte"
import ComponentTree from "./ComponentTree.svelte" import ComponentTree from "./ComponentTree.svelte"
import { dndStore } from "./dndStore.js" import { dndStore } from "./dndStore.js"
import { goto } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { store, selectedScreen } from "builderStore" import { store, selectedScreen, selectedComponent } 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 { setContext, onMount } from "svelte"
import { get } from "svelte/store"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { DropPosition } from "./dndStore" import { DropPosition } from "./dndStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifications, Button } from "@budibase/bbui" import { notifications, Button } from "@budibase/bbui"
let scrollRef let scrollRef
let confirmDeleteDialog
const scrollTo = bounds => { const scrollTo = bounds => {
if (!bounds) { if (!bounds) {
@ -69,6 +72,76 @@
setContext("scroll", { setContext("scroll", {
scrollTo, scrollTo,
}) })
const deleteComponent = async () => {
await store.actions.components.delete(get(selectedComponent))
}
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
}
const component = get(selectedComponent)
try {
if (e.ctrlKey || e.metaKey) {
if (e.key === "ArrowUp") {
e.preventDefault()
await store.actions.components.moveUp(component)
} else if (e.key === "ArrowDown") {
e.preventDefault()
await store.actions.components.moveDown(component)
} else if (e.key === "c") {
e.preventDefault()
await store.actions.components.copy(component, false)
} else if (e.key === "x") {
e.preventDefault()
store.actions.components.copy(component, true)
} else if (e.key === "v") {
e.preventDefault()
await store.actions.components.paste(component, "inside")
} else if (e.key === "d") {
e.preventDefault()
await store.actions.components.copy(component)
await store.actions.components.paste(component, "below")
} else if (e.key === "Enter") {
e.preventDefault()
$goto("./new")
}
} else if (e.key === "Backspace" || e.key === "Delete") {
// Don't show confirmation for the screen itself
if (component._id === get(selectedScreen).props._id) {
return
}
e.preventDefault()
confirmDeleteDialog.show()
} else if (e.key === "ArrowUp") {
e.preventDefault()
await store.actions.components.selectPrevious()
} else if (e.key === "ArrowDown") {
e.preventDefault()
await store.actions.components.selectNext()
} else if (e.key === "Escape" && $isActive("./new")) {
e.preventDefault()
$goto("./")
}
} catch (error) {
console.log(error)
notifications.error("Error handling key press")
}
}
onMount(() => {
document.addEventListener("keydown", handleKeyPress)
return () => {
document.removeEventListener("keydown", handleKeyPress)
}
})
</script> </script>
<Panel title="Components" showExpandIcon borderRight> <Panel title="Components" showExpandIcon borderRight>
@ -116,6 +189,13 @@
</ul> </ul>
</div> </div>
</Panel> </Panel>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you want to delete "${$selectedComponent?._instanceName}"?`}
okText="Delete Component"
onOk={deleteComponent}
/>
<style> <style>
.add-component { .add-component {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.2.47", "version": "1.2.57",
"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.2.47", "@budibase/backend-core": "1.2.57",
"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",

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,8 @@
"devicePreview": true, "devicePreview": true,
"messagePassing": true, "messagePassing": true,
"rowSelection": true, "rowSelection": true,
"continueIfAction": true "continueIfAction": true,
"showNotificationAction": true
}, },
"layout": { "layout": {
"name": "Layout", "name": "Layout",
@ -237,6 +238,11 @@
"showInBar": true, "showInBar": true,
"barIcon": "ModernGridView", "barIcon": "ModernGridView",
"barTitle": "Wrap" "barTitle": "Wrap"
},
{
"type": "event",
"label": "On Click",
"key": "onClick"
} }
] ]
}, },
@ -1466,10 +1472,11 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Colours", "label": "Colors",
"key": "palette", "key": "palette",
"defaultValue": "Palette 1", "defaultValue": "Palette 1",
"options": [ "options": [
"Custom",
"Palette 1", "Palette 1",
"Palette 2", "Palette 2",
"Palette 3", "Palette 3",
@ -1482,6 +1489,51 @@
"Palette 10" "Palette 10"
] ]
}, },
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{ {
"type": "boolean", "type": "boolean",
"label": "Stacked", "label": "Stacked",
@ -1581,10 +1633,11 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Colours", "label": "Colors",
"key": "palette", "key": "palette",
"defaultValue": "Palette 1", "defaultValue": "Palette 1",
"options": [ "options": [
"Custom",
"Palette 1", "Palette 1",
"Palette 2", "Palette 2",
"Palette 3", "Palette 3",
@ -1597,6 +1650,51 @@
"Palette 10" "Palette 10"
] ]
}, },
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{ {
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
@ -1695,10 +1793,11 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Colours", "label": "Colors",
"key": "palette", "key": "palette",
"defaultValue": "Palette 1", "defaultValue": "Palette 1",
"options": [ "options": [
"Custom",
"Palette 1", "Palette 1",
"Palette 2", "Palette 2",
"Palette 3", "Palette 3",
@ -1711,6 +1810,51 @@
"Palette 10" "Palette 10"
] ]
}, },
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{ {
"type": "select", "type": "select",
"label": "Curve", "label": "Curve",
@ -1800,10 +1944,11 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Colours", "label": "Colors",
"key": "palette", "key": "palette",
"defaultValue": "Palette 1", "defaultValue": "Palette 1",
"options": [ "options": [
"Custom",
"Palette 1", "Palette 1",
"Palette 2", "Palette 2",
"Palette 3", "Palette 3",
@ -1816,6 +1961,51 @@
"Palette 10" "Palette 10"
] ]
}, },
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{ {
"type": "boolean", "type": "boolean",
"label": "Data Labels", "label": "Data Labels",
@ -1882,10 +2072,11 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Colours", "label": "Colors",
"key": "palette", "key": "palette",
"defaultValue": "Palette 1", "defaultValue": "Palette 1",
"options": [ "options": [
"Custom",
"Palette 1", "Palette 1",
"Palette 2", "Palette 2",
"Palette 3", "Palette 3",
@ -1898,6 +2089,51 @@
"Palette 10" "Palette 10"
] ]
}, },
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{ {
"type": "boolean", "type": "boolean",
"label": "Data Labels", "label": "Data Labels",
@ -2875,6 +3111,12 @@
"key": "timeOnly", "key": "timeOnly",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "24-Hour time",
"key": "time24hr",
"defaultValue": false
},
{ {
"type": "boolean", "type": "boolean",
"label": "Ignore time zones", "label": "Ignore time zones",

View file

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.2.47", "version": "1.2.57",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.2.47", "@budibase/bbui": "^1.2.57",
"@budibase/frontend-core": "^1.2.47", "@budibase/frontend-core": "^1.2.57",
"@budibase/string-templates": "^1.2.47", "@budibase/string-templates": "^1.2.57",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View file

@ -94,7 +94,7 @@
id="spectrum-root" id="spectrum-root"
lang="en" lang="en"
dir="ltr" dir="ltr"
class="spectrum spectrum--medium spectrum--darkest {$themeStore.theme}" class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
> >
<DeviceBindingsProvider> <DeviceBindingsProvider>
<UserBindingsProvider> <UserBindingsProvider>

View file

@ -17,10 +17,16 @@
--spectrum-semantic-cta-color-background-default: var(--primaryColor); --spectrum-semantic-cta-color-background-default: var(--primaryColor);
--spectrum-semantic-cta-color-background-hover: var(--primaryColorHover); --spectrum-semantic-cta-color-background-hover: var(--primaryColorHover);
--spectrum-semantic-cta-color-background-down: var(--primaryColorHover); --spectrum-semantic-cta-color-background-down: var(--primaryColorHover);
--spectrum-button-primary-s-border-radius: var(--buttonBorderRadius); --spectrum-button-primary-s-border-radius: calc(
var(--buttonBorderRadius) * 0.9
);
--spectrum-button-primary-m-border-radius: var(--buttonBorderRadius); --spectrum-button-primary-m-border-radius: var(--buttonBorderRadius);
--spectrum-button-primary-l-border-radius: var(--buttonBorderRadius); --spectrum-button-primary-l-border-radius: calc(
--spectrum-button-primary-xl-border-radius: var(--buttonBorderRadius); var(--buttonBorderRadius) * 1.25
);
--spectrum-button-primary-xl-border-radius: calc(
var(--buttonBorderRadius) * 1.5
);
/* Loading spinners */ /* Loading spinners */
--spectrum-progresscircle-medium-track-fill-color: var(--primaryColor); --spectrum-progresscircle-medium-track-fill-color: var(--primaryColor);

View file

@ -10,6 +10,7 @@
export let size export let size
export let gap export let gap
export let wrap export let wrap
export let onClick
$: directionClass = direction ? `valid-container direction-${direction}` : "" $: directionClass = direction ? `valid-container direction-${direction}` : ""
$: hAlignClass = hAlign ? `hAlign-${hAlign}` : "" $: hAlignClass = hAlign ? `hAlign-${hAlign}` : ""
@ -25,7 +26,13 @@
].join(" ") ].join(" ")
</script> </script>
<div class={classNames} use:styleable={$component.styles} class:wrap> <div
class={classNames}
class:clickable={!!onClick}
use:styleable={$component.styles}
class:wrap
on:click={onClick}
>
<slot /> <slot />
</div> </div>
@ -104,4 +111,10 @@
.wrap { .wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.clickable {
cursor: pointer;
}
.clickable :global(*) {
pointer-events: none;
}
</style> </style>

View file

@ -10,7 +10,9 @@
</script> </script>
{#if options} {#if options}
<div use:chart={options} use:styleable={$component.styles} /> {#key options.customColor}
<div use:chart={options} use:styleable={$component.styles} />
{/key}
{:else if $builderStore.inBuilder} {:else if $builderStore.inBuilder}
<div use:styleable={$component.styles}> <div use:styleable={$component.styles}>
<Placeholder /> <Placeholder />

View file

@ -62,8 +62,14 @@ export class ApexOptionsBuilder {
return this.setOption(["title", "text"], title) return this.setOption(["title", "text"], title)
} }
color(color) { colors(colors) {
return this.setOption(["colors"], [color]) if (!colors) {
delete this.options.colors
this.options["customColor"] = false
return this
}
this.options["customColor"] = true
return this.setOption(["colors"], colors)
} }
width(width) { width(width) {

View file

@ -16,6 +16,7 @@
export let stacked export let stacked
export let yAxisUnits export let yAxisUnits
export let palette export let palette
export let c1, c2, c3, c4, c5
export let horizontal export let horizontal
$: options = setUpChart( $: options = setUpChart(
@ -33,9 +34,13 @@
stacked, stacked,
yAxisUnits, yAxisUnits,
palette, palette,
horizontal horizontal,
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
customColor
) )
$: customColor = palette === "Custom"
const setUpChart = ( const setUpChart = (
title, title,
dataProvider, dataProvider,
@ -51,7 +56,9 @@
stacked, stacked,
yAxisUnits, yAxisUnits,
palette, palette,
horizontal horizontal,
colors,
customColor
) => { ) => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if ( if (
@ -85,6 +92,7 @@
.stacked(stacked) .stacked(stacked)
.palette(palette) .palette(palette)
.horizontal(horizontal) .horizontal(horizontal)
.colors(customColor ? colors : null)
// Add data // Add data
let useDates = false let useDates = false

View file

@ -17,6 +17,7 @@
export let legend export let legend
export let yAxisUnits export let yAxisUnits
export let palette export let palette
export let c1, c2, c3, c4, c5
// Area specific props // Area specific props
export let area export let area
@ -40,9 +41,13 @@
palette, palette,
area, area,
stacked, stacked,
gradient gradient,
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
customColor
) )
$: customColor = palette === "Custom"
const setUpChart = ( const setUpChart = (
title, title,
dataProvider, dataProvider,
@ -60,7 +65,9 @@
palette, palette,
area, area,
stacked, stacked,
gradient gradient,
colors,
customColor
) => { ) => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if ( if (
@ -96,6 +103,7 @@
.legend(legend) .legend(legend)
.yUnits(yAxisUnits) .yUnits(yAxisUnits)
.palette(palette) .palette(palette)
.colors(customColor ? colors : null)
// Add data // Add data
let useDates = false let useDates = false

View file

@ -13,6 +13,7 @@
export let legend export let legend
export let donut export let donut
export let palette export let palette
export let c1, c2, c3, c4, c5
$: options = setUpChart( $: options = setUpChart(
title, title,
@ -25,9 +26,13 @@
animate, animate,
legend, legend,
donut, donut,
palette palette,
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
customColor
) )
$: customColor = palette === "Custom"
const setUpChart = ( const setUpChart = (
title, title,
dataProvider, dataProvider,
@ -39,7 +44,9 @@
animate, animate,
legend, legend,
donut, donut,
palette palette,
colors,
customColor
) => { ) => {
if ( if (
!dataProvider || !dataProvider ||
@ -70,6 +77,7 @@
.legend(legend) .legend(legend)
.legendPosition("right") .legendPosition("right")
.palette(palette) .palette(palette)
.colors(customColor ? colors : null)
// Add data if valid datasource // Add data if valid datasource
const series = data.map(row => parseFloat(row[valueColumn])) const series = data.map(row => parseFloat(row[valueColumn]))

View file

@ -47,6 +47,17 @@
} }
} }
const deleteAttachments = async fileList => {
try {
return await API.deleteAttachments({
keys: fileList,
tableId: formContext?.dataSource?.tableId,
})
} catch (error) {
return []
}
}
const handleChange = e => { const handleChange = e => {
fieldApi.setValue(e.detail) fieldApi.setValue(e.detail)
if (onChange) { if (onChange) {
@ -72,6 +83,7 @@
error={fieldState.error} error={fieldState.error}
on:change={handleChange} on:change={handleChange}
{processFiles} {processFiles}
{deleteAttachments}
{handleFileTooLarge} {handleFileTooLarge}
{handleTooManyFiles} {handleTooManyFiles}
{maximum} {maximum}

View file

@ -8,6 +8,7 @@
export let disabled = false export let disabled = false
export let enableTime = false export let enableTime = false
export let timeOnly = false export let timeOnly = false
export let time24hr = false
export let ignoreTimezones = false export let ignoreTimezones = false
export let validation export let validation
export let defaultValue export let defaultValue
@ -44,6 +45,7 @@
appendTo={document.getElementById("flatpickr-root")} appendTo={document.getElementById("flatpickr-root")}
{enableTime} {enableTime}
{timeOnly} {timeOnly}
{time24hr}
{ignoreTimezones} {ignoreTimezones}
{placeholder} {placeholder}
/> />

View file

@ -5,6 +5,8 @@ import "@spectrum-css/vars/dist/spectrum-darkest.css"
import "@spectrum-css/vars/dist/spectrum-dark.css" import "@spectrum-css/vars/dist/spectrum-dark.css"
import "@spectrum-css/vars/dist/spectrum-light.css" import "@spectrum-css/vars/dist/spectrum-light.css"
import "@spectrum-css/vars/dist/spectrum-lightest.css" import "@spectrum-css/vars/dist/spectrum-lightest.css"
import "@budibase/frontend-core/src/themes/nord.css"
import "@budibase/frontend-core/src/themes/midnight.css"
import "@spectrum-css/page/dist/index-vars.css" import "@spectrum-css/page/dist/index-vars.css"
// Non user-facing components // Non user-facing components

View file

@ -16,20 +16,20 @@
}) })
const onKeyDown = e => { const onKeyDown = e => {
if (e.key === "Delete" || e.key === "Backspace") {
deleteSelectedComponent()
}
}
const deleteSelectedComponent = () => {
const state = get(builderStore) const state = get(builderStore)
if (!state.inBuilder || !state.selectedComponentId || state.editMode) { if (!state.inBuilder || state.editMode) {
return return
} }
const activeTag = document.activeElement?.tagName.toLowerCase() const activeTag = document.activeElement?.tagName.toLowerCase()
if (["input", "textarea"].indexOf(activeTag) !== -1) { if (["input", "textarea"].indexOf(activeTag) !== -1) {
return return
} }
builderStore.actions.deleteComponent(state.selectedComponentId)
// Need to manually block certain keys from propagating to the browser
if (e.ctrlKey && e.key === "d") {
e.preventDefault()
}
builderStore.actions.keyDown(e.key, e.ctrlKey)
} }
</script> </script>

View file

@ -40,8 +40,8 @@ const createBuilderStore = () => {
updateProp: (prop, value) => { updateProp: (prop, value) => {
dispatchEvent("update-prop", { prop, value }) dispatchEvent("update-prop", { prop, value })
}, },
deleteComponent: id => { keyDown: (key, ctrlKey) => {
dispatchEvent("delete-component", { id }) dispatchEvent("key-down", { key, ctrlKey })
}, },
duplicateComponent: id => { duplicateComponent: id => {
dispatchEvent("duplicate-component", { id }) dispatchEvent("duplicate-component", { id })

View file

@ -62,10 +62,14 @@ const createNotificationStore = () => {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
send, send,
info: msg => send(msg, "info", "Info"), info: (msg, autoDismiss) =>
success: msg => send(msg, "success", "CheckmarkCircle"), send(msg, "info", "Info", autoDismiss ?? true),
warning: msg => send(msg, "warning", "Alert"), success: (msg, autoDismiss) =>
error: msg => send(msg, "error", "Alert", false), send(msg, "success", "CheckmarkCircle", autoDismiss ?? true),
warning: (msg, autoDismiss) =>
send(msg, "warning", "Alert", autoDismiss ?? true),
error: (msg, autoDismiss) =>
send(msg, "error", "Alert", autoDismiss ?? false),
blockNotifications, blockNotifications,
dismiss, dismiss,
}, },

View file

@ -1,6 +1,7 @@
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { appStore } from "./app" import { appStore } from "./app"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { Constants } from "@budibase/frontend-core"
// This is the good old acorn bug where having the word "g l o b a l" makes it // This is the good old acorn bug where having the word "g l o b a l" makes it
// think that this is not ES6 compatible and starts throwing errors when using // think that this is not ES6 compatible and starts throwing errors when using
@ -28,6 +29,13 @@ const createThemeStore = () => {
// Ensure theme is set // Ensure theme is set
theme = theme || defaultTheme theme = theme || defaultTheme
// Get base theme
let base =
Constants.Themes.find(x => `spectrum--${x.class}` === theme)?.base || ""
if (base) {
base = `spectrum--${base}`
}
// Delete and nullish keys from the custom theme // Delete and nullish keys from the custom theme
if (customTheme) { if (customTheme) {
Object.entries(customTheme).forEach(([key, value]) => { Object.entries(customTheme).forEach(([key, value]) => {
@ -51,6 +59,7 @@ const createThemeStore = () => {
return { return {
theme, theme,
baseTheme: base,
customTheme, customTheme,
customThemeCss, customThemeCss,
} }

View file

@ -300,6 +300,14 @@ const continueIfHandler = action => {
} }
} }
const showNotificationHandler = action => {
const { message, type, autoDismiss } = action.parameters
if (!message || !type) {
return
}
notificationStore.actions[type]?.(message, autoDismiss)
}
const handlerMap = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler, ["Duplicate Row"]: duplicateRowHandler,
@ -318,6 +326,7 @@ const handlerMap = {
["Upload File to S3"]: s3UploadHandler, ["Upload File to S3"]: s3UploadHandler,
["Export Data"]: exportDataHandler, ["Export Data"]: exportDataHandler,
["Continue if / Stop if"]: continueIfHandler, ["Continue if / Stop if"]: continueIfHandler,
["Show Notification"]: showNotificationHandler,
} }
const confirmTextMap = { const confirmTextMap = {
@ -334,8 +343,8 @@ const confirmTextMap = {
*/ */
export const enrichButtonActions = (actions, context) => { export const enrichButtonActions = (actions, context) => {
// Prevent button actions in the builder preview // Prevent button actions in the builder preview
if (!actions || get(builderStore).inBuilder) { if (!actions?.length || get(builderStore).inBuilder) {
return () => {} return null
} }
// If this is a function then it has already been enriched // If this is a function then it has already been enriched

View file

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.2.47", "version": "1.2.57",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.2.47", "@budibase/bbui": "^1.2.57",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View file

@ -61,5 +61,32 @@ export const buildAttachmentEndpoints = API => {
}) })
return { publicUrl } return { publicUrl }
}, },
/**
* Deletes attachments from the bucket.
* @param keys the attachments to delete
* @param tableId the associated table ID
*/
deleteAttachments: async ({ keys, tableId }) => {
return await API.post({
url: `/api/attachments/${tableId}/delete`,
body: {
keys,
},
})
},
/**
* Deletes attachments from the builder bucket.
* @param keys the attachments to delete
*/
deleteBuilderAttachments: async keys => {
return await API.post({
url: `/api/attachments/delete`,
body: {
keys,
},
})
},
} }
} }

View file

@ -39,13 +39,17 @@ export const OperatorOptions = {
label: "Contains", label: "Contains",
}, },
NotContains: { NotContains: {
value: "notEqual", value: "notContains",
label: "Does Not Contain", label: "Does Not Contain",
}, },
In: { In: {
value: "oneOf", value: "oneOf",
label: "Is in", label: "Is in",
}, },
ContainsAny: {
value: "containsAny",
label: "Has any",
},
} }
// Cookie names // Cookie names

View file

@ -12,5 +12,7 @@
--spectrum-global-color-gray-700: hsl(var(--hue), var(--sat), 70%); --spectrum-global-color-gray-700: hsl(var(--hue), var(--sat), 70%);
--spectrum-global-color-gray-800: hsl(var(--hue), var(--sat), 80%); --spectrum-global-color-gray-800: hsl(var(--hue), var(--sat), 80%);
--spectrum-global-color-gray-900: hsl(var(--hue), var(--sat), 95%); --spectrum-global-color-gray-900: hsl(var(--hue), var(--sat), 95%);
--modal-background: var(--spectrum-global-color-gray-50);
} }

View file

@ -43,4 +43,7 @@
--spectrum-alias-highlight-hover: rgba(169, 177, 193, 0.1); --spectrum-alias-highlight-hover: rgba(169, 177, 193, 0.1);
--spectrum-alias-highlight-active: rgba(169, 177, 193, 0.1); --spectrum-alias-highlight-active: rgba(169, 177, 193, 0.1);
--spectrum-alias-background-color-hover-overlay: rgba(169, 177, 193, 0.1);
--modal-background: var(--spectrum-global-color-gray-50);
} }

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